С архитектурой приложений часто возникают вопросы. Это касается как приложений пакетной обработки (batch job), веб-приложений, так и приложений с обменом сообщениями (messaging application) и других. Фреймворки, такие как Spring Batch, Spring Webflux и Spring Integration служат ориентиром в процессе принятия решения. Кроме того, существует множество специализированных фреймворков, предназначенных для определенной предметной области. Но в этом посте мы не будем о них говорить, а рассмотрим варианты конфигурации Spring.

Помните, что Spring — это большой мешок с объектами. Чтобы предоставлять сервисы, Spring должен знать, как организованы объекты: как они связаны между собой и как взаимодействуют. Например, Spring может стартовать транзакцию при входе в метод и завершить ее при выходе из метода. Или создать HTTP-эндпоинты, которые вызывают методы контроллеров при поступлении запросов. А также обрабатывать сообщения от брокеров, таких как Apache Kafka, AWS SQS, RabbitMQ и других. Список возможностей Spring можно продолжать и продолжать, но все это предполагает первоначальную регистрацию объектов в Spring.

Spring хранит метамодель ваших объектов — это что-то вроде Java Reflection API. Он знает, какие у классов есть аннотации и конструкторы. Также ему известно о зависимостях конкретного объекта: от каких бинов и типов зависит объект. Ваша задача — помочь ему построить эту метамодель для управления всеми вашими объектами. Например, при возможности управления созданием объектов, можно изменить процесс создания объектов до того, как они будут созданы.

Spring предоставит вам все эти сервисы только в случае, если будет знать, как объекты связаны между собой. Таким образом, идея состоит в том, что вы используете POJO (Plain Old Java Objects), а Spring ищет в них аннотации и использует их для настройки поведения ваших сервисов. Но, конечно, это не сделать без контроля создания объектов.

Spring за кулисами создает новый класс, который расширяет ваш. Делается это либо через создание Java InvocationHandler (JDK-прокси), либо, что бывает чаще, с помощью чего-то вроде CGLIB. Создаваемый класс является подклассом вашего класса. Итак, допустим, у вас есть следующий класс:

class CustomerService  {

	private final JdbcTemplate template; 

	CustomerService (JdbcTemplate jt) {
		this.JdbcTemplate = jt;
	}

    @Transactional 
	public void updateCustomer ( long customerId, String name){
       // .. .
	}
}

Например, вам нужен автоматический запуск и завершение транзакции при каждом вызове метода updateCustomer. Чтобы это работало, Spring должен вставить свой код до и после вызова этого метода. За кулисами происходит примерно следующее:

class SpringEnhancedCustomerService extends CustomerService {

    // Spring provides a reference from the applicationContext of type JdbcTemplate
	SpringEnhancedCustomerService (JdbcTemplate jt) {
		 super(JdbcTemplate ) ;
	}


	@Override 
	public void updateCustomer (long customerId, String name) {
		// call Java code to start a JDBC transaction 
		super.updateCustomer(customerId, name);
		// call Java code to stop a JDBC transaction
	}
}

Затем в своем коде вы можете инжектировать ссылку на CustomerService. Сервис вы получите, но это будет не тот класс, который вы создавали. Вместо своего класса у вас будет подкласс. Вот такой фокус — вы просите шляпу, а получаете вместо нее шляпу с кроликом. Это и делает Spring таким мощным.

Итак, Spring должен знать о ваших объектах.

До появления Spring Boot у вас было два стандартных варианта конфигурации: XML и Java. Однако это было давно. В настоящее время XML не приветствуется, и остается Java-конфигурация. Вот пример класса конфигурации:

@Configuration 
class ServiceConfiguration {

 @Bean DataSource h2DataSource (){
 	return ... ;
 }

 @Bean JdbcTemplate JdbcTemplate (DataSource ds) {
 	return new JdbcTemplate(ds);
 }

  @Bean CustomerService customerService (JdbcTemplate jdbcTemplate) {
  	return new CustomerService (jdbcTemplate);
  }
}

Здесь создается три объекта и они явно связываются между собой. Spring при старте ищет классы с аннотацией @Configuration, вызывает все методы, аннотированные @Bean, сохраняет все возвращаемые значения в контекст и делает их доступными для инъекции. Если метод принимает параметры, то он ищет другой метод, который возвращает значение нужного типа, и сначала вызывает его. Затем это значение инжектируется в метод через параметр. Если метод ранее уже вызывался для какой-то другой инъекции, то используется уже созданный экземпляр.

Преимущество такого подхода в его явности: вся информация о том, как ваши объекты связаны между собой, находится в одном месте — в классах конфигурации. Как вы заметили, информация о ваших классах присутствует в двух разных местах: в самом классе и в классе конфигурации.

Поэтому можно использовать другой, менее явный подход: сканирование компонентов (component-scanning). В этом случае Spring ищет классы в classpath с аннотациями стереотипов, таких как @Component или @Controller. Все аннотации стереотипов в конечном счете аннотируются @Component. Аннотация @Component — самая общая. Если вы посмотрите на @Controller, он аннотирован @Component. Если вы посмотрите на @RestController, то он тоже будет аннотирован @Controller. Класс, аннотированный @RestController, по-прежнему обрабатывается как минимум как класс, аннотированный @Component. Специализированные аннотации уточняют функциональность, но они по-прежнему остаются наследниками аннотации @Component, а не альтернативой ей.

Итак, кажется, описывать класс CustomerService в классе конфигурации будет лишним. В конце концов, если Spring знает только об этом классе, он дальше сам может разобраться в его связях, не так ли? Можно взглянуть на конструктор и увидеть, что для создания экземпляра CustomerService потребуется ссылка на JdbcTemplate, который определен в другом месте.

Сканирование компонентов это и делает. Вы можете добавить на класс аннотацию @Service — еще одну стереотипную аннотацию, помеченную @Component, — а затем удалить @Bean-метод в классе конфигурации. Spring автоматически создаст сервис и предоставит необходимые зависимости. Он также создаст подкласс для реализации необходимых сервисов.

Мы делаем успехи, удаляя все больше бойлерплейта. Но как насчет DataSource и JdbcTemplate? Они нам нужны, но не описывать же их каждый раз заново? В этом нам поможет Spring Boot. Для принятия решения о создании класса или вызова @Bean-метода Spring Boot использует аннотацию @Condition для декорирования классов с @Component или @Configuration. Решение может приниматься и на основе окружения. Например, у вас в classpath есть H2 (встраиваемая SQL-база данных) и библиотека spring-jdbc, которая содержит класс JdbcTemplate. При наличии этих классов в classpath можно сделать вывод о том, что вам нужен встроенный SQL DataSource и что вы хотите связать экземпляр JdbcTemplate с этим источником данных. Теперь можно полностью отказаться от класса @Configuration!

Мы рассмотрели основы конфигурации Spring IoC-контейнера. Можно было бы пойти намного дальше и поговорить об аспектно-ориентированном программировании (АОП), автоконфигурации и многом другом. Но об этом не сегодня. Цель этого поста — объяснить, когда применять тот или иной вид конфигурации.


Материал подготовлен в рамках курса «Разработчик на Spring Framework». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.