В примере Hello, Spring! контекст Spring создавался с использованием аннотаций, таких как @Service, и специального класса, описывающего конфигурацию контекста Spring, смешивая два разных подхода к конфигурирования контекста.
В настоящий момент Spring framework поддерживает четыре разных способа конфигурирования контекста, каждый из которых заслуживает отдельного рассмотрения.
Подготовка
Я создал многомодульный maven проект, с различными подмодулями для каждого варианта конфигурирования контекста. За основу каждого модуля я взял код примера Hello, Spring! и изменил его соответствующим образом.
1
2
3
4
5
6
|
<modules>
<module>xml</module>
<module>annotations</module>
<module>javaconfig</module>
<module>groovy</module>
</modules>
|
XML
Исторически конфигурирование контекста c использованием XML было первым методом конфигурирования, появившемся в Spring.
Конфигурирование с помощью XML заключается в создании xml файла (традиционно носящего названия вида «context.xml», «applicationContext.xml» и т.д.), описывающего Spring beans, процесс их создания и взаимосвязи между ними. Поэтому в первую очередь убедимся, что в коде отсутствуют аннотации @Service и @Inject и сразу после их удаления напишем им замену в xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean id="coin" class="ru.easyjava.spring.CoinImpl">
<constructor-arg type="java.util.Random">
<bean class="java.util.Random"/>
</constructor-arg>
</bean>
<bean id="target" class="ru.easyjava.spring.GreeterTargetImpl">
<constructor-arg index="0" ref="coin"/>
</bean>
<bean id="greeter" class="ru.easyjava.spring.Greeter">
<constructor-arg name="newTarget" ref="target"/>
</bean>
</beans>
|
Файл «/resource/applicationContext.xml» заменяет и класс ContextConfiguration и аннотации.
Вначале мы определили бин «coin», передав ему в конструктор внутренний анонимный бин, созданный из java.util.Random. Связывание было проведено по типу аргумента, а передаваемый в конструктор бин был определён на месте.
Бин «coin» используется в следующем определении. GreeterTargetImpl требует, чтобы ему передали экземпляр Coin и мы его передаём. Причём в этот раз передаём его как параметр номер 0 и, вместо создания бина на месте, мы ссылаемся на ранее определённый бин с именем «coin».
Бин «greeter» демонстрирует третий метод передачи параметров — по имени переменной.
Кроме параметров конструктора в xml конфиге можно сразу устанавливать значения свойств, методы вызываемые при инициализации бина, использовать фабрики для создания экземпляров бинов, но всему этому я посвящу отдельную статью, а пока обойдусь простым примером:
1
2
3
4
|
<bean id="example" class="ru.easyjava.spring.ExampleWithProperty">
<constructor-arg name="newTarget" ref="target"/>
<property name="coin" ref="coin"/>
</bean>
|
Последним действием надо будет исправить код интеграционного теста и само приложение. В тесте заменим @ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes = ru.easyjava.spring.ContextConfiguration.class) на @ContextConfiguration("/applicationContext.xml").
В приложении заменим создание контекста на ClassPathXmlApplicationContext("/applicationContext.xml") и можно проверять результат:
1
2
3
4
5
6
7
8
|
>mvn integration-test
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.413 sec - in ru.easyjava.spring.AppIT
>java -jar target/xml-1-jar-with-dependencies.jar
org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@7cd84586: startup date [Fri Jul 31 16:40:26 EEST 2015]; root of context hierarchy
org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [applicationContext.xml]
Hello World
|
Использованием XML конфигурации конечно неудобно тем, что приходится кроме написания непосредственно кода описывать ещё и его использование в Spring, при этом не имея возможности проверить ошибки в конфигурации до запуска тестов (или даже приложения). Кроме того, в декларативном xml файле достаточно сложно реализовать конфигурацию, требующую активных действий во время создания контекста.
С другой стороны, xml конфигурация представляет собой централизованное описание приложения, которое может хранится отдельно от кода, позволяя менять структуру приложения без пересборки. С помощью xml можно использовать в качестве Spring beans сторонний код. И, что для кого-то может оказаться важным, в коде отсутствуют специфичные для Spring вещи.
Annotations
Конфигурирование Spring context с помощью аннотаций уже рассматривалось в Hello, Spring!, но полезно будет попробовать аннотации в чистом виде.
К коду оригинального примера добавится ещё один класс:
1
2
3
4
|
@Service
public class RandomImpl extends Random {
static final long serialVersionUID = 1L;
}
|
Который нужен только лишь для того, чтобы «навесить» аннотацию @Service на java.util.Random. В коде самого приложения аргумент AnnotationConfigApplicationContext меняется на название package, в котором искать бины:
1
|
new AnnotationConfigApplicationContext("ru.easyjava.spring");
|
Код интеграционного теста придётся сильно изменить. Дело в том, что в настоящий момент @ConfigurationContext не поддерживает annotation-only конфигурацию, поэтому приходится использовать костыль:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class AppIT {
@Inject
private ApplicationContext context;
@Configuration
@ComponentScan("ru.easyjava.spring")
public static class SpringConfig { }
@Test
public void testSpring() {
Greeter greeter = context.getBean(Greeter.class);
assertTrue(greeter.greet().startsWith("Hello"));
}
}
|
Конечно, использование аннотаций для определения бинов и их зависимостей, весьма удобно и упрощает разработку, но недостатков у этого подхода больше всего. Конфигурация контекста получается децентрализованной, так что неосторожное добавление нового бина может внезапно изменить работу всего приложения (и это, опять таки, не узнать до запуска тестов/приложения). Так же в код попадают Spring специфичные вещи, которые потом могут затруднить смену платформы. С аннотациями использование стороннего кода либо невозможно, либо требует определённых подпорок и костылей. Кроме того, единственной возможностью изменить поведение приложения будет его пересборка.
Java configuration
Третий подход — программное создание бинов, реализующий модный принцип convention over configuration, соглашения по конфигурации. Стоит отметить, что под программным созданием бинов я понимаю создание бинов на стадии формирования контекста, а не после того, как приложение уже запущено.
Так же, как и в случае с xml подходом, начнём с удаления аннотаций @Service и @Inject. Все бины будут теперь определяться в ContextConfiguration:
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
|
public class ContextConfiguration {
/**
* "Random" service bean.
* @return Java's built-in random generator.
*/
@Bean
public Random random() {
return new Random();
}
@Bean
public Coin coin() {
return new CoinImpl(random());
}
@Bean
public GreeterTarget greeterTarget() {
return new GreeterTargetImpl(coin());
}
@Bean
public Greeter greeter() {
return new Greeter(greeterTarget());
}
}
|
Spring вызывает в конфигурационных классах методы с аннотацией @Bean. Объекты, возвращённые этими методами, регистрируются как Spring бины. Названия бинов соответствуют названиям методов, которые их порождают.
Вызов методов, создающих бины, вручную, вполне безопасен, потому что Spring изменяет код создания бина, чтобы пытаться вернуть уже существующий подходящий бин и только если это невозможно, вызывать создающий код. Поэтому в методе coin() не создаётся второй экземпляр бина random, а используется ранее созданый бин. По этой причине методы, имеющие аннотацию @Bean, не должны объявляться final.
Важно и название класса конфигурации: несмотря на искушение назвать конфигурационный класс Context, делать этого не следует. Дело в том, что Spring создаст бин и из этого класса, а в качестве имени использует имя класса. А имя «Context» уже занято самим Spring.
Java конфигурация выглядит наилучшим образом, если сравнивать её достоинства и недостатки. Это и централизованность как в xml; и безопасность типов; и простой рефакторинг; и отсутствия spring специфичных вещей в коде; и возможность выполнения каких-либо действий на этапе конфигурации. К недостаткам, пожалуй, относится необходимость ручного создания бинов и необходимость пересборки для переконфигурации приложения.
Groovy
Поддержка Groovy скриптов и специального beans DSL появилась в четвёртой версии Spring как попытка сделать конфигурацию вообще без недостатков. Groovy конфигурация должна объединить в себе достоинства XML конфигурации и Java конфигурации, оставаясь дружественной к аннотациям.
Чтобы попробовать groovy конфигурацию, необходимо добавить groovy runtime в приложение:
1
2
3
4
5
6
7
8
9
10
11
|
<properties>
<groovy.version>2.4.4</groovy.version>
</properties>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
</dependency>
</dependencies>
|
Класс ContextConfiguration заменим скриптом «/resources/applicationContext.groovy»:
1
2
3
4
5
6
|
beans {
random(Random)
xmlns([ctx:'http://www.springframework.org/schema/context'])
ctx.'component-scan'('base-package':"ru.easyjava.spring")
}
|
Минимальные изменения внесём в классы приложения:
1
2
|
ApplicationContext context =
new GenericGroovyApplicationContext("/applicationContext.groovy");
|
и интеграционного теста:
1
|
@ContextConfiguration("/applicationContext.groovy")
|
И проверим, что оно всё ещё работает:
1
2
3
4
5
6
7
8
|
>mvn verify
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.956 sec - in ru.easyjava.spring.AppIT
>java -jar target/groovy-1-jar-with-dependencies.jar
org.springframework.context.support.GenericGroovyApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.GenericGroovyApplicationContext@7cd84586: startup date [Fri Jul 31 18:16:56 EEST 2015]; root of context hierarchy
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init>
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Hello Spring
|
К достоинствам groovy конфигурации можно отнести всё хорошее, что есть в XML и Java подходах: централизованное описание приложения, которое может хранится отдельно от кода, позволяя менять структуру приложения без пересборки (а в перспективе без перезапуска); использование стороннего кода в качестве Spring beans; возможность сохранить код чистым от Spring аннотаций; безопасность типов и простота рефакторинга; выполнение кода на этапе конфигурирования. Однако поддержка groovy конфигурации ещё недостаточно доработана и не все возможности, достижимые с помощью xml/java, доступны с groovy.
Какой подход выбрать?
Используйте все сразу, они совместимы. Аннотации уменьшают накладные расходы на конфигурацию и не заставляют разработчика переключаться между написанием кода и конфигурацией. Java конфигурация хороша для стороннего кода или какой-либо сложной инициализации. XML позволяет вынести конфигурацию контекста за пределы приложения, делая возможным настройку контекста на этапе эксплуатации. Groovy заменит и java и xml, но пока он не достаточно хорош.
Код примера доступен на github.