Spring Framework — многофункциональный фреймворк для Java, состоящий из нескольких крупных модулей и предоставляющий различные сервисы java разработчикам.
Центральная концепция фреймворка — IoC контейнер, управляющий объектами, и конфигурационный контекст (context), описывающий приложение и дополнительную функциональность.
Подготовка
Вначале создадим проект с помощью maven:
[INFO] Scanning for projects... [INFO] BUILD SUCCESSFUL [INFO] Total time: 28 seconds
И добавим к нему Spring и JUnit:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <javaee.version>7.0</javaee.version> <org.springframework.version>4.1.7.RELEASE</org.springframework.version> <junit.version>4.12</junit.version> <hamcrest.version>1.3</hamcrest.version> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>${hamcrest.version}</version> <scope>test</scope> </dependency> </dependencies>
Помимо зависимостей, понадобятся ещё два плагина, один для сборки jar файла с зависимостями, другой для запуска интеграционных тестов:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>ru.easyjava.spring.App</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.18.1</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Приложение
Как заведено при изучении нового в программировании, первым делом надо поздороваться с пользователем. Приветствовать пользователя мы будем с использованием целых трёх сервисов: один будет подбрасывать монетку, а два других выбирать, как и с кем здороваться.
Начнём с монетки:
/** * Coin, that could be tossed. */ public interface Coin { /** * Here we toss the coin. * @return unpredicted true of false. */ boolean toss(); }
/** * A simple implementation of Coin, * based on Random class. */ @Service public class CoinImpl implements Coin { /** * Random data source. */ private Random random; /** * Simple constructor. * @param newRandom Supplied random generator. */ @Inject public CoinImpl(final Random newRandom) { this.random = newRandom; } /** * Here we toss the coin. * @return unpredicted true of false. */ @Override public final boolean toss() { return random.nextBoolean(); } }
Монетку надо покрыть юнит-тестами, но протестировать её не так просто, нам придётся написать свою собственую реализацию, дублёра Random, поведением которой мы сможем задавать. Такая реализация называется stub:
public class StubRandom extends Random { private boolean constantResult; public final void setConstantResult(final boolean newResult) { this.constantResult = newResult; } @Override public boolean nextBoolean() { return constantResult; } }
public class CoinTest { @Test public void testToss() throws Exception { /** Prepare the mock */ StubRandom random = new StubRandom(); /** Prepare the object */ Coin testedObject = new CoinImpl(random); /** Test it! */ random.setConstantResult(true); assertTrue(testedObject.toss()); random.setConstantResult(false); assertFalse(testedObject.toss()); } }
На основе броска монетки выберем, кого приветствовать:
/** * Here we determine, who we are greeting today. */ public interface GreeterTarget { /** * Selects greeting target tossing a coin. * @return "World" or "Spring". */ String get(); }
/** * Simple implementation with * hardcoded targets. */ @Service public class GreeterTargetImpl implements GreeterTarget { /** * Coin, we toss to define greeting target. */ private Coin coin; /** * Simple constructor. * @param newCoin Coin, that we will be tossing. */ @Inject public GreeterTargetImpl(final Coin newCoin) { this.coin = newCoin; } /** * Selects greeting target tossing a coin. * @return "World" or "Spring". */ @Override public final String get() { if (coin.toss()) { return "World"; } return "Spring"; } }
Чтобы протестировать этот сервис, нам понадобиться дублёр монетки и именно для этого она была разделена на интерфейс и его реализацию, так как для теста нам нужна другая реализация:
public class StubCoin implements Coin { private boolean constantResult; public final void setConstantResult(final boolean newResult) { this.constantResult = newResult; } @Override public boolean toss() { return constantResult; } }
public class GreeterTargetTest { @Test public void testGet() throws Exception { /* Prepare the mock */ StubCoin coin = new StubCoin(); /* Prepare the Object */ GreeterTarget testedObject = new GreeterTargetImpl(coin); /* Test it! */ coin.setConstantResult(true); assertEquals("World", testedObject.get()); coin.setConstantResult(false); assertEquals("Spring", testedObject.get()); } }
Наконец, напишем сам класс приветствия:
/** * Greeting service. */ @Service public class Greeter { /** * Here we ask, who we are greeting. */ private GreeterTarget target; /** * Simple constructor. * @param newTarget Greeter target selector to use. */ @Inject public Greeter(final GreeterTarget newTarget) { this.target = newTarget; } /** * Generates greeting. * @return "Hello-style" string. */ public final String greet() { return "Hello " + target.get(); } }
Для теста сервиса приветствия придётся написать тестовую реализацию GreeterTarget:
public class StubGreeterTarget implements GreeterTarget { @Override public String get() { return "TEST"; } }
public class GreeterTest { @Test public void testGreet() throws Exception { /* Prepare the mock */ GreeterTarget target = new StubGreeterTarget(); /* Prepare the Object */ Greeter testedObject = new Greeter(target); /* Test it! */ assertEquals("Hello TEST", testedObject.greet()); } }
Проверим что все тесты успешны и перейдём непосредственно к Spring:
------------------------------------------------------- T E S T S ------------------------------------------------------- Running ru.easyjava.spring.CoinTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec - in ru.easyjava.spring.CoinTest Running ru.easyjava.spring.GreeterTargetTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in ru.easyjava.spring.GreeterTargetTest Running ru.easyjava.spring.GreeterTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in ru.easyjava.spring.GreeterTest Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Spring
Я сразу, при написании кода, пометил каждый класс аннотацией @Service, объявляющей класс Spring bean. С этой аннотацией жизненным циклом этих классов управляет Spring и нам не надо беспокоиться о их создании. Но чтобы Spring смог создать объекты этих классов, а в классах нет конструкторов по умолчанию, каждый конструктор имеет аннотацию @Inject.
Сочетание этих аннотаций говорит Spring’у: «Возьми класс и создай из него объект. При создании поищи существующие объекты подходящего типа и передай их в конструктор». Самая настоящая инверсия управления: сервисы говорят «Дай-ка мне реализацию», а Spring уже подбирает реализацию из доступных и отдаёт её сервисам при создании. Необходимость в ручном связывании компонентов отпала.
Однако у внимательного читателя возникает вопрос: Greeter зависит от GreeterTarget, который создаст Spring. GreeterTarget зависит от Coin, которую тоже создась Spring. Coin зависит от Random, а кто создаст объект Random? Очевидно, что это должен быть Spring, но как? Это библиотечный класс и к нему нельзя добавить аннотацию @Service. Зато его можно создать вручную, в специальном конфигурационном классе:
/** * Spring context configuration descriptor. */ @Configuration @ComponentScan("ru.easyjava.spring") public class ContextConfiguration { /** * "Random" service bean. * @return Java's built-in random generator. */ @Bean public Random random() { return new Random(); } }
Конфигурационный класс, помеченный аннотацией @Configuration, настраивает контекст исполнения Spring, в том числе и добавляя к нему дополнительные Spring beans.
Аннотация @ComponentScan(«ru.easyjava.spring») говорит Spring’у, что необходимо просканировать классы в пакете ru.easyjava.spring на наличие Spring аннотаций, таких как @Service, и обработать их.
Интеграционный тест
Теперь, когда все компоненты приложения готовы, можно проверить, как они взаимодействуют друг с другом.
Spring включает в себя небольшой инструментарий для упрощения тестирования и, в частности, поддержку загрузки контекста в JUnit тестах. Используя специальный runner SpringJUnit4ClassRunner, мы инициализируем Spring контест автоматически при запуске теста, а аннотация @ContextConfiguration указывает, как именно мы хотим сконфигурировать контекст.
@ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes = ru.easyjava.spring.ContextConfiguration.class) @RunWith(SpringJUnit4ClassRunner.class) public class AppIT { @Inject private ApplicationContext context; @Test public void testSpring() { Greeter greeter = context.getBean(Greeter.class); assertTrue(greeter.greet().startsWith("Hello")); } }
Обратите внимание, что класс интеграционного теста имеет суффикс *IT, а не *Test. По соглашению все классы имеющие суффикс *IT признаются maven’ом интеграционными тестами и запускаются отдельно от юнит-тестов:
mvn integration-test INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.323 sec - in ru.easyjava.spring.AppIT Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Сразу видно, что даже такой простой тест исполняется на два порядке медленнее, чем модульный тест. Поэтому их и запускают отдельно.
Приложение
Тесты проходят, давайте запускать приложение:
/** * Application main class. */ public final class App { /** * Do not construct me. */ private App() { }; /** * Application entry point. * @param args Array of command line arguments. */ public static void main(final String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class); Greeter greeter = context.getBean(Greeter.class); System.out.println(greeter.greet()); } }
Разберём класс приложения детально:
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);
Создаёт Spring context используя аннотации и Spring beans из ContextConfiguration.
Greeter greeter = context.getBean(Greeter.class);
Запрашивает из контекста bean типа Greeter. Стоит отметить, что класс к этому времени уже сконструирован, классы, от которых он зависит, тоже уже сконструированы и getBean только возвращает ссылку на существующий экземпляр.
В последней строке мы используем bean Greeter по назначению:
System.out.println(greeter.greet());
Проверим результат:
>mvn verify [INFO] ---------------------- [INFO] BUILD SUCCESS [INFO] ---------------------- [INFO] Total time: 14.483 s >java -jar target/hellospring-1-jar-with-dependencies.jar 9:21:14 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7530d0a: startup date [Thu Jul 16 21:21:14 EEST 2015]; root of context hierarchy 9:21:14 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init> INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring Hello World >java -jar target/hellospring-1-jar-with-dependencies.jar 9:21:18 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7530d0a: startup date [Thu Jul 16 21:21:18 EEST 2015]; root of context hierarchy 9:21:18 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init> INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring Hello Spring
Мы видим как запускается IoC контейнер Spring и потом отрабатывает сервис приветствия, выдавая разные приветствия с каждым запуском. Всё получилось ????