Статьи

Как работает автоконфигурация Spring Boot

В моем предыдущем посте « Почему Spring Boot? » Мы рассмотрели, как создать приложение Spring Boot, но вы можете или не можете понять, что происходит за кулисами. Возможно, вы захотите понять магию автоконфигурации Spring Boot.

Перед этим вы должны знать о функции SpringConnect @Conditional , от которой зависит вся магия автоконфигурации Spring Boot.

Изучение силы @Conditional 

При разработке приложений на основе Spring может возникнуть необходимость условной регистрации bean-компонентов.

Например, вы можете зарегистрировать bean-компонент DataSource, указывающий на базу данных dev при локальном запуске приложения, и указывать на другую производственную базу данных при работе в рабочей среде. 

Вы можете перенести параметры соединения с базой данных в файл свойств и использовать файл, соответствующий среде, но вам нужно изменить конфигурацию всякий раз, когда вам нужно указать другую среду и создать приложение.

Для решения этой проблемы в Spring 3.1 была введена концепция профилей . Вы можете зарегистрировать несколько бинов одного типа и связать их с одним или несколькими профилями. При запуске приложения вы можете активировать нужные профили и компоненты, связанные с активированными профилями, и только эти профили будут зарегистрированы.

@Configuration
public class AppConfig
{
 @Bean
 @Profile("DEV")
 public DataSource devDataSource() {
 ...
 }

 @Bean
 @Profile("PROD")
 public DataSource prodDataSource() {
 ...
 }
}

Затем вы можете указать активный профиль, используя Системное свойство -Dspring.profiles.active = DEV.

Этот подход работает для простых случаев, таких как включение или отключение регистрации компонентов на основе активированных профилей. Но если вы хотите зарегистрировать bean-компоненты, основанные на некоторой условной логике, тогда сам подход профилей недостаточен.

Чтобы обеспечить большую гибкость условной регистрации bean-компонентов Spring, Spring 4 ввел концепцию @Conditional . Используя  подход @Conditional, вы можете зарегистрировать компонент, условно основанный на любом произвольном условии.

Например, вы можете зарегистрировать компонент, когда:

  • Определенный класс присутствует в classpath
  • Spring bean определенного типа еще не зарегистрирован в ApplicationContext
  • Определенный файл существует в местоположении
  • Конкретное значение свойства настраивается в файле конфигурации
  • Определенное системное свойство присутствует / отсутствует

Это всего лишь несколько примеров, и вы можете иметь любое условие, которое захотите.

Давайте посмотрим, как работает Spring’s @Conditional.

Предположим, у нас есть интерфейс UserDAO  с методами для получения данных из хранилища данных. У нас есть две реализации интерфейса UserDAO,  а именно JdbcUserDAO,  который общается с базой данных MySQL,  и MongoUserDAO,  который общается с MongoDB .

Мы можем захотеть включить только один интерфейс JdbcUserDAO  и MongoUserDAO  на основе системного свойства, скажем, dbType .

Если приложение запускается с использованием java -jar myapp.jar -DdbType = MySQL , то мы хотим включить JdbcUserDAO.  В противном случае, если приложение запускается с использованием java -jar myapp.jar -DdbType = MONGO , мы хотим включить MongoUserDAO .

Предположим , что мы имеем  UserDAO  фасоль и  JdbcUserDAO фасоль. MongoUserDAO  реализация выглядит следующим образом :

public interface UserDAO
{
 List<String> getAllUserNames();
}

public class JdbcUserDAO implements UserDAO
{
 @Override
 public List<String> getAllUserNames()
 {
 System.out.println("**** Getting usernames from RDBMS *****");
 return Arrays.asList("Siva","Prasad","Reddy");
 }
}

public class MongoUserDAO implements UserDAO
{
 @Override
 public List<String> getAllUserNames()
 {
 System.out.println("**** Getting usernames from MongoDB *****");
 return Arrays.asList("Bond","James","Bond");
 }
}

Мы можем реализовать Условие MySQLDatabaseTypeCondition  проверить , является ли система собственности DBTYPE  является «MYSQL» выглядит следующим образом :

public class MySQLDatabaseTypeCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
 {
 String enabledDBType = System.getProperty("dbType");
 return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
 }
}

Мы можем реализовать Условие MongoDBDatabaseTypeCondition,  чтобы проверить, имеет ли системное свойство dbType значение  « MONGODB » следующим образом:

public class MongoDBDatabaseTypeCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
 {
 String enabledDBType = System.getProperty("dbType");
 return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MONGODB"));
 }
}

Теперь мы можем условно настроить bean- компоненты JdbcUserDAO  и MongoUserDAO,  используя @Conditional следующим образом:

@Configuration
public class AppConfig
{
 @Bean
 @Conditional(MySQLDatabaseTypeCondition.class)
 public UserDAO jdbcUserDAO(){
 return new JdbcUserDAO();
 }

 @Bean
 @Conditional(MongoDBDatabaseTypeCondition.class)
 public UserDAO mongoUserDAO(){
 return new MongoUserDAO();
 }
}

Если мы запустим приложение наподобие  java -jar myapp.jar -DdbType = MYSQL, тогда будет зарегистрирован только  bean- компонент JdbcUserDAO. Но  если вы установите свойство System, например -DdbType = MONGODB, то будет зарегистрирован только  bean- компонент MongoUserDAO  .

Теперь, когда мы увидели, как условно зарегистрировать бин на основе системного свойства.

Предположим, что мы хотим зарегистрировать bean- компонент MongoUserDAO  только в том случае , если в classpath доступен класс драйвера Java MongoDB  «com.mongodb.Server» , если нет, мы хотим зарегистрировать bean- компонент JdbcUserDAO  .

Для этого мы можем создать условия для проверки наличия или отсутствия класса драйвера MongoDB «com.mongodb.Server» следующим образом:

public class MongoDriverPresentsCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext,AnnotatedTypeMetadata metadata)
 {
 try {
 Class.forName("com.mongodb.Server");
 return true;
 } catch (ClassNotFoundException e) {
 return false;
 }
 }
}

public class MongoDriverNotPresentsCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
 {
 try {
 Class.forName("com.mongodb.Server");
 return false;
 } catch (ClassNotFoundException e) {
 return true;
 }
 }
}

Мы только что увидели, как зарегистрировать bean-компоненты условно на основании наличия или отсутствия класса в classpath.

Что если мы хотим зарегистрировать  bean- компонент MongoUserDAO,  только если другой Spring-компонент типа UserDAO  уже не зарегистрирован.

Мы можем создать Условие, чтобы проверить, существует ли какой-либо bean-компонент определенного типа, следующим образом:

public class UserDAOBeanNotPresentsCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
 {
 UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
 return (userDAO == null);
 }
}

Что если мы хотим зарегистрировать bean- компонент MongoUserDAO,  только если в файле конфигурации заполнителя свойств установлено свойство app.dbType = MONGO ?

Мы можем реализовать это условие следующим образом:

public class MongoDbTypePropertyCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext,
 AnnotatedTypeMetadata metadata)
 {
 String dbType = conditionContext.getEnvironment()
 .getProperty("app.dbType");
 return "MONGO".equalsIgnoreCase(dbType);
 }
}

Мы только что увидели, как реализовать различные типы Условий. Но есть еще более элегантный способ реализовать Условия с использованием Аннотаций. Вместо создания реализации Condition для MYSQL и MongoDB мы можем создать аннотацию aDatabaseType следующим образом:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType
{
 String value();
}

Затем мы можем реализовать DatabaseTypeCondition,  чтобы использовать значение DatabaseType,  чтобы определить, включить или отключить регистрацию компонента следующим образом:

public class DatabaseTypeCondition implements Condition
{
 @Override
 public boolean matches(ConditionContext conditionContext,
 AnnotatedTypeMetadata metadata)
 {
 Map<String, Object> attributes = metadata.getAnnotationAttributes(DatabaseType.class.getName());
 String type = (String) attributes.get("value");
 String enabledDBType = System.getProperty("dbType","MYSQL");
 return (enabledDBType != null && type != null && enabledDBType.equalsIgnoreCase(type));
 }
}

Теперь мы можем использовать аннотацию @DatabaseType в наших определениях компонентов следующим образом:

@Configuration
@ComponentScan
public class AppConfig
{
 @DatabaseType("MYSQL")
 public UserDAO jdbcUserDAO(){
 return new JdbcUserDAO();
 }

 @Bean
 @DatabaseType("MONGO")
 public UserDAO mongoUserDAO(){
 return new MongoUserDAO();
 }
}

Здесь мы получаем метаданные из аннотации DatabaseType  и проверяем значение dbType  системного свойства, чтобы определить, включить или отключить регистрацию компонента.

Мы видели множество примеров, чтобы понять, как мы можем зарегистрировать бины условно, используя аннотацию @Conditional .

Spring Boot широко использует функцию @Conditional для регистрации bean-компонентов условно на основе различных критериев.

Вы можете найти различные реализации Условий, которые SpringBoot использует в org.springframework.boot.autoconfigure в  пакете spring-boot-autoconfigure- {version} .jar .

Теперь, когда мы узнали о том, как Spring Boot использует  функцию @Conditional для условной проверки, регистрировать бин или нет, но что именно запускает механизм автоконфигурации?

Это то, что мы рассмотрим в следующем разделе.

Автоконфигурация Spring Boot 

Ключ к волшебству автоконфигурации Spring Boot — аннотация @EnableAutoConfiguration . Обычно мы аннотируем наш класс точки входа Application либо с помощью @SpringBootApplication, либо, если мы хотим настроить значения по умолчанию, мы можем использовать следующие аннотации:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{

}

@EnableAutoConfiguration аннотаций позволяет автоматическую конфигурацию Spring ApplicationContext  путем сканирования компонентов пути к классам и регистрирует бобы, которые соответствующие различные условия.

SpringBoot предоставляет различные классы AutoConfiguration  в spring-boot-autoconfigure- {version} .jar , которые отвечают за регистрацию различных компонентов.

Обычно автонастройки  классов аннотированные с @Configuration , чтобы пометить его как класс конфигурации Spring и аннотированный с @EnableConfigurationProperties , чтобы связывать свойство настройки и один или более условных методы регистрации фасоли.

Например, рассмотрим  класс org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration .

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration 
{
 ...
 ...
 @Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class)
 @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
 @Import(EmbeddedDataSourceConfiguration.class)
 protected static class EmbeddedConfiguration {

 }

 @Configuration
 @ConditionalOnMissingBean(DataSourceInitializer.class)
 protected static class DataSourceInitializerConfiguration {
 @Bean
 public DataSourceInitializer dataSourceInitializer() {
 return new DataSourceInitializer();
 }
 }

 @Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class)
 @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
 protected static class NonEmbeddedConfiguration {
 @Autowired
 private DataSourceProperties properties;

 @Bean
 @ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
 public DataSource dataSource() {
 DataSourceBuilder factory = DataSourceBuilder
 .create(this.properties.getClassLoader())
 .driverClassName(this.properties.getDriverClassName())
 .url(this.properties.getUrl()).username(this.properties.getUsername())
 .password(this.properties.getPassword());
 if (this.properties.getType() != null) {
 factory.type(this.properties.getType());
 }
 return factory.build();
 }
 }
 ...
 ...
 @Configuration
 @ConditionalOnProperty(prefix = "spring.datasource", name = "jmx-enabled")
 @ConditionalOnClass(name = "org.apache.tomcat.jdbc.pool.DataSourceProxy")
 @Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
 @ConditionalOnMissingBean(name = "dataSourceMBean")
 protected static class TomcatDataSourceJmxConfiguration {
 @Bean
 public Object dataSourceMBean(DataSource dataSource) {
 ....
 ....
 }
 }
 ...
 ...
}

Здесь  DataSourceAutoConfiguration  помечается @ConditionalOnClass ({DataSource.class, EmbeddedDatabaseType.class}) , который означает , что AutoConfiguration бобов в DataSourceAutoConfiguration  будет рассматриваться только если  DataSource.class  и EmbeddedDatabaseType.class  классы доступны по классам.

Класс также аннотируется @EnableConfigurationProperties (DataSourceProperties.class),  который позволяет автоматически связывать свойства в application.properties со свойствами класса DataSourceProperties .

@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {

 public static final String PREFIX = "spring.datasource";
 ...
 ...
 private String driverClassName;
 private String url;
 private String username;
 private String password;
 ...
 //setters and getters
}

При такой конфигурации все свойства, которые начинаются с spring.datasource. *, Будут автоматически привязаны к объекту DataSourceProperties  .

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Вы также можете увидеть некоторые внутренние классы и методы определения bean-компонентов, которые аннотированы условными аннотациями SpringBoot, такими как @ConditionalOnMissingBean, @ConditionalOnClass, @ConditionalOnProperty и т. Д.

Эти определения bean-компонентов будут зарегистрированы в ApplicationContext,  только если эти условия будут выполнены.

Вы также можете изучить многие другие классы автоконфигурации в spring-boot-autoconfigure- {version} .jar, такие как:

  • org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration 
  • org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration 
  • org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration 
  • org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationetc и т. д. 

Я надеюсь, что теперь у вас есть понимание того, как работает автоконфигурация Spring Boot, используя различные классы автоконфигурации вместе с функциями @Conditional .