Spring Framework — многофункциональный фреймворк для Java, состоящий из нескольких крупных модулей и предоставляющий различные сервисы java разработчикам.
Центральная концепция фреймворка — IoC контейнер, управляющий объектами, и конфигурационный контекст (context), описывающий приложение и дополнительную функциональность.
Подготовка
Вначале создадим проект с помощью maven:
1
2
3
|
[INFO] Scanning for projects...
[INFO] BUILD SUCCESSFUL
[INFO] Total time: 28 seconds
|
И добавим к нему Spring и JUnit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
<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 файла с зависимостями, другой для запуска интеграционных тестов:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
<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>
|
Приложение
Как заведено при изучении нового в программировании, первым делом надо поздороваться с пользователем. Приветствовать пользователя мы будем с использованием целых трёх сервисов: один будет подбрасывать монетку, а два других выбирать, как и с кем здороваться.
Начнём с монетки:
1
2
3
4
5
6
7
8
9
10
|
/**
* Coin, that could be tossed.
*/
public interface Coin {
/**
* Here we toss the coin.
* @return unpredicted true of false.
*/
boolean toss();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/**
* 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:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class StubRandom extends Random {
private boolean constantResult;
public final void setConstantResult(final boolean newResult) {
this.constantResult = newResult;
}
@Override
public boolean nextBoolean() {
return constantResult;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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());
}
}
|
На основе броска монетки выберем, кого приветствовать:
1
2
3
4
5
6
7
8
9
10
|
/**
* Here we determine, who we are greeting today.
*/
public interface GreeterTarget {
/**
* Selects greeting target tossing a coin.
* @return "World" or "Spring".
*/
String get();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/**
* 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";
}
}
|
Чтобы протестировать этот сервис, нам понадобиться дублёр монетки и именно для этого она была разделена на интерфейс и его реализацию, так как для теста нам нужна другая реализация:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class StubCoin implements Coin {
private boolean constantResult;
public final void setConstantResult(final boolean newResult) {
this.constantResult = newResult;
}
@Override
public boolean toss() {
return constantResult;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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());
}
}
|
Наконец, напишем сам класс приветствия:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 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:
1
2
3
4
5
6
|
public class StubGreeterTarget implements GreeterTarget {
@Override
public String get() {
return "TEST";
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-------------------------------------------------------
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. Зато его можно создать вручную, в специальном конфигурационном классе:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* 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 указывает, как именно мы хотим сконфигурировать контекст.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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’ом интеграционными тестами и запускаются отдельно от юнит-тестов:
1
2
3
4
5
6
7
|
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
|
Сразу видно, что даже такой простой тест исполняется на два порядке медленнее, чем модульный тест. Поэтому их и запускают отдельно.
Приложение
Тесты проходят, давайте запускать приложение:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* 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());
}
}
|
Разберём класс приложения детально:
1
2
|
ApplicationContext context =
new AnnotationConfigApplicationContext(ContextConfiguration.class);
|
Создаёт Spring context используя аннотации и Spring beans из ContextConfiguration.
1
|
Greeter greeter = context.getBean(Greeter.class);
|
Запрашивает из контекста bean типа Greeter. Стоит отметить, что класс к этому времени уже сконструирован, классы, от которых он зависит, тоже уже сконструированы и getBean только возвращает ссылку на существующий экземпляр.
В последней строке мы используем bean Greeter по назначению:
1
|
System.out.println(greeter.greet());
|
Проверим результат:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
>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 и потом отрабатывает сервис приветствия, выдавая разные приветствия с каждым запуском. Всё получилось 🙂
Код примера доступен на github.