Статьи

Как работает магия автоконфигурации SpringBoot?

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

Но перед этим вы должны знать о функции SpringCondition @Conditional, от которой зависит вся магия автоконфигурации SpringBoot.

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Configuration
public class AppConfig
{
    @Bean
    @Profile("DEV")
    public DataSource devDataSource() {
        ...
    }
 
    @Bean
    @Profile("PROD")
    public DataSource prodDataSource() {
        ...
    }
}

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

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

Чтобы обеспечить гораздо большую гибкость условной регистрации 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 следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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» следующим образом:

1
2
3
4
5
6
7
8
9
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 » следующим образом:

1
2
3
4
5
6
7
8
9
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 следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@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, тогда будет зарегистрирован только бин JdbcUserDAO . Но если вы -DdbType = MONGODB, то будет зарегистрирован только bean- компонент MongoUserDAO .

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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, только если никакой другой bean-компонент Spring типа UserDAO уже не зарегистрирован.

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

1
2
3
4
5
6
7
8
9
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 ?

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

01
02
03
04
05
06
07
08
09
10
11
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 следующим образом:

1
2
3
4
5
6
7
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType
{
    String value();
}

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

01
02
03
04
05
06
07
08
09
10
11
12
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 в наших определениях компонентов следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@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 .

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

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

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

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

SpringBoot AutoConfiguration

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

1
2
3
4
5
6
7
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{
 
}

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

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

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

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

01
02
03
04
05
06
07
08
09
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@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}), что означает, что автоматическая конфигурация bean-компонентов в DataSourceAutoConfiguration будет рассматриваться только в том случае, если классы DataSource.class и EmbeddedDatabaseType.class доступны на classpath.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
@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 .

1
2
3
4
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.JacksonAutoConfiguration и т. д. и т. д.

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