• Spring Boot自动配置如何工作


    通过使用Mongo和MySQL DB实现的示例,深入了解Spring Boot的@Conditional注释世界。

    在我以前的文章“为什么选择Spring Boot?”中,我们讨论了如何创建Spring Boot应用程序,但是你可能了解也可能不了解幕后情况。 你可能想了解Spring Boot自动配置背后的魔力。

    在此之前,你应该了解Spring的@Conditional功能,所有Spring Boot的AutoConfiguration魔术都依赖于此。

     

    探索@Conditional的力量

    在开发基于Spring的应用程序时,我们可能会遇到有条件地注册bean的需求。

    例如,你可能想在本地运行应用程序时注册一个指向dev数据库的DataSource bean,而在生产环境中运行时则指向一个不同的生产数据库。

    你可以将数据库连接参数外部化到属性文件中,并使用适合该环境的文件,但是每当需要指向其他环境并构建应用程序时,都需要更改配置。

    为了解决这个问题,Spring 3.1引入了Profiles的概念。 你可以注册多个相同类型的bean,并将它们与一个或多个概要文件关联。 运行应用程序时,你可以激活所需的配置文件以及与已激活的配置文件相关联的Bean,并且只有那些配置文件将被注册。

     1 @Configuration
     2 public class AppConfig
     3 {
     4  @Bean
     5  @Profile("DEV")
     6  public DataSource devDataSource() {
     7  ...
     8  }
     9  @Bean
    10  @Profile("PROD")
    11  public DataSource prodDataSource() {
    12  ...
    13  }
    14 }

    然后,你可以使用系统属性-Dspring.profiles.active=DEV指定活动配置文件。

    这种方法适用于简单情况,例如根据已激活的配置文件启用或禁用bean注册。但是,如果你要基于某些条件逻辑来注册bean,那么概要文件方法本身是不够的。

    为了为有条件地注册Spring Bean提供更大的灵活性,Spring 4引入了@Conditional的概念。通过使用@Conditional方法,你可以根据任意条件有条件地注册bean。

    例如,在以下情况下,你可能想要注册一个bean:

    • 类路径中存在特定的类
    • 某些类型的Spring bean尚未在ApplicationContext中注册
    • 某个位置上存在特定文件
    • 在配置文件中配置了特定的属性值
    • 存在/不存在特定的系统属性

    这些仅是几个示例,你可以具有所需的任何条件。

    让我们看一下Spring的@Conditional的工作原理。

    假设我们有一个UserDAO接口,其中包含从数据存储中获取数据的方法。我们有两种UserDAO接口的实现,即与MySQL数据库对话的JdbcUserDAO和与MongoDB对话的MongoUserDAO。

    我们可能只想基于系统属性(即dbType)启用JdbcUserDAO和MongoUserDAO的一个接口。

    如果使用java -jar myapp.jar -DdbType = MySQL启动应用程序,那么我们要启用JdbcUserDAO。否则,如果使用java -jar myapp.jar -DdbType = MONGO启动了应用程序,则我们要启用MongoUserDAO。

    假设我们有一个UserDAO bean和一个JdbcUserDAO bean。 MongoUserDAO实现如下:

     1 public interface UserDAO
     2 {
     3  List<String> getAllUserNames();
     4 }
     5 public class JdbcUserDAO implements UserDAO
     6 {
     7  @Override
     8  public List<String> getAllUserNames()
     9  {
    10  System.out.println("**** Getting usernames from RDBMS *****");
    11  return Arrays.asList("Siva","Prasad","Reddy");
    12  }
    13 }
    14 public class MongoUserDAO implements UserDAO
    15 {
    16  @Override
    17  public List<String> getAllUserNames()
    18  {
    19  System.out.println("**** Getting usernames from MongoDB *****");
    20  return Arrays.asList("Bond","James","Bond");
    21  }
    22 }

    我们可以实现条件MySQLDatabaseTypeCondition来检查系统属性dbType是否为“ MYSQL”,如下所示:

    1 public class MySQLDatabaseTypeCondition implements Condition
    2 {
    3  @Override
    4  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    5  {
    6  String enabledDBType = System.getProperty("dbType");
    7  return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
    8  }
    9 }

    我们可以实现条件MongoDBDatabaseTypeCondition来检查系统属性dbType是否为“ MONGODB”,如下所示:

    1 public class MongoDBDatabaseTypeCondition implements Condition
    2 {
    3  @Override
    4  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    5  {
    6  String enabledDBType = System.getProperty("dbType");
    7  return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MONGODB"));
    8  }
    9 }

    现在,我们可以使用@Conditional有条件地配置JdbcUserDAO和MongoUserDAO Bean,如下所示:

     1 @Configuration
     2 public class AppConfig
     3 {
     4  @Bean
     5  @Conditional(MySQLDatabaseTypeCondition.class)
     6  public UserDAO jdbcUserDAO(){
     7  return new JdbcUserDAO();
     8  }
     9  @Bean
    10  @Conditional(MongoDBDatabaseTypeCondition.class)
    11  public UserDAO mongoUserDAO(){
    12  return new MongoUserDAO();
    13  }
    14 }

    如果运行类似java -jar myapp.jar -DdbType = MYSQL的应用程序,则仅会注册JdbcUserDAO Bean。但是,如果将系统属性设置为-DdbType = MONGODB,则只会注册MongoUserDAO Bean。

    现在,我们已经看到了如何基于系统属性有条件地注册bean。

    假设仅当classpath上有MongoDB Java驱动程序类“ com.mongodb.Server”可用时,我们才想注册MongoUserDAO Bean,否则,我们希望注册JdbcUserDAO Bean。

    为此,我们可以创建条件来检查MongoDB驱动程序类“ com.mongodb.Server”的存在与否,如下所示:

     1 public class MongoDriverPresentsCondition implements Condition
     2 {
     3  @Override
     4  public boolean matches(ConditionContext conditionContext,AnnotatedTypeMetadata metadata)
     5  {
     6  try {
     7  Class.forName("com.mongodb.Server");
     8  return true;
     9  } catch (ClassNotFoundException e) {
    10  return false;
    11  }
    12  }
    13 }
    14 public class MongoDriverNotPresentsCondition implements Condition
    15 {
    16  @Override
    17  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    18  {
    19  try {
    20  Class.forName("com.mongodb.Server");
    21  return false;
    22  } catch (ClassNotFoundException e) {
    23  return true;
    24  }
    25  }
    26 }

    我们刚刚看到了如何根据类路径中是否存在类来有条件地注册bean。

    如果仅当尚未注册其他类型为UserDAO的Spring Bean时才想注册MongoUserDAO Bean,该怎么办?

    我们可以创建一个条件来检查是否存在某种特定类型的现有bean,如下所示:

    1 public class UserDAOBeanNotPresentsCondition implements Condition
    2 {
    3  @Override
    4  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    5  {
    6  UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
    7  return (userDAO == null);
    8  }
    9 }

    如果仅在属性占位符配置文件中设置属性app.dbType = MONGO时才想注册MongoUserDAO bean,该怎么办?

    我们可以如下实现该条件:

     1 public class MongoDbTypePropertyCondition implements Condition
     2 {
     3  @Override
     4  public boolean matches(ConditionContext conditionContext,
     5  AnnotatedTypeMetadata metadata)
     6  {
     7  String dbType = conditionContext.getEnvironment()
     8  .getProperty("app.dbType");
     9  return "MONGO".equalsIgnoreCase(dbType);
    10  }
    11 }

    我们刚刚看到了如何实现各种类型的条件,但是还有使用注释来实现条件的更优雅的方法。 代替为MYSQL和MongoDB创建Condition实现,我们可以创建如下的DatabaseType批注:

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

    然后,我们可以实现DatabaseTypeCondition以使用DatabaseType值来确定是启用还是禁用bean注册,如下所示:

     1 public class DatabaseTypeCondition implements Condition
     2 {
     3  @Override
     4  public boolean matches(ConditionContext conditionContext,
     5  AnnotatedTypeMetadata metadata)
     6  {
     7  Map<String, Object> attributes = metadata.getAnnotationAttributes(DatabaseType.class.getName());
     8  String type = (String) attributes.get("value");
     9  String enabledDBType = System.getProperty("dbType","MYSQL");
    10  return (enabledDBType != null && type != null && enabledDBType.equalsIgnoreCase(type));
    11  }
    12 }

    现在,我们可以在bean定义上使用@DatabaseType批注,如下所示:

     1 @Configuration
     2 @ComponentScan
     3 public class AppConfig
     4 {
     5  @DatabaseType("MYSQL")
     6  public UserDAO jdbcUserDAO(){
     7  return new JdbcUserDAO();
     8  }
     9  @Bean
    10  @DatabaseType("MONGO")
    11  public UserDAO mongoUserDAO(){
    12  return new MongoUserDAO();
    13  }
    14 }

    在这里,我们从DatabaseType批注中获取元数据,并对照System Property dbType值进行检查,以确定是启用还是禁用Bean注册。

    我们已经看到了很多示例,以了解如何使用@Conditional批注有条件地注册bean。

    Spring Boot广泛使用@Conditional功能根据各种条件有条件地注册bean。

    你可以在spring-boot-autoconfigure- {version} .jar的org.springframework.boot.autoconfigure包中找到SpringBoot使用的各种Condition实现。

    现在我们已经知道了Spring Boot如何使用@Conditional功能有条件地检查是否注册Bean,但是究竟是什么触发了自动配置机制呢?

    这就是我们将在下一部分中讨论的内容。

    Spring Boot自动配置

    Spring Boot自动配置魔术的关键是@EnableAutoConfiguration批注。通常,我们使用@SpringBootApplication注释应用程序入口点类,或者,如果要自定义默认值,可以使用以下注释:

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

    @EnableAutoConfiguration批注通过扫描类路径组件并注册与各种条件匹配的bean来启用Spring ApplicationContext的自动配置。

    SpringBoot在spring-boot-autoconfigure- {version} .jar中提供了各种AutoConfiguration类,这些类负责注册各种组件。

    通常,AutoConfiguration类使用@Configuration注释,以将其标记为Spring配置类,并使用@EnableConfigurationProperties注释,以绑定定制属性和一个或多个条件Bean注册方法。

    例如,考虑org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration类。

     1 @Configuration
     2 @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
     3 @EnableConfigurationProperties(DataSourceProperties.class)
     4 @Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
     5 public class DataSourceAutoConfiguration 
     6 {
     7  ...
     8  ...
     9  @Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class)
    10  @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    11  @Import(EmbeddedDataSourceConfiguration.class)
    12  protected static class EmbeddedConfiguration {
    13  }
    14  @Configuration
    15  @ConditionalOnMissingBean(DataSourceInitializer.class)
    16  protected static class DataSourceInitializerConfiguration {
    17  @Bean
    18  public DataSourceInitializer dataSourceInitializer() {
    19  return new DataSourceInitializer();
    20  }
    21  }
    22  @Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class)
    23  @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    24  protected static class NonEmbeddedConfiguration {
    25  @Autowired
    26  private DataSourceProperties properties;
    27  @Bean
    28  @ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
    29  public DataSource dataSource() {
    30  DataSourceBuilder factory = DataSourceBuilder
    31  .create(this.properties.getClassLoader())
    32  .driverClassName(this.properties.getDriverClassName())
    33  .url(this.properties.getUrl()).username(this.properties.getUsername())
    34  .password(this.properties.getPassword());
    35  if (this.properties.getType() != null) {
    36  factory.type(this.properties.getType());
    37  }
    38  return factory.build();
    39  }
    40  }
    41  ...
    42  ...
    43  @Configuration
    44  @ConditionalOnProperty(prefix = "spring.datasource", name = "jmx-enabled")
    45  @ConditionalOnClass(name = "org.apache.tomcat.jdbc.pool.DataSourceProxy")
    46  @Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
    47  @ConditionalOnMissingBean(name = "dataSourceMBean")
    48  protected static class TomcatDataSourceJmxConfiguration {
    49  @Bean
    50  public Object dataSourceMBean(DataSource dataSource) {
    51  ....
    52  ....
    53  }
    54  }
    55  ...
    56  ...
    57 }

    在这里,DataSourceAutoConfiguration带有@ConditionalOnClass({DataSource.class,EmbeddedDatabaseType.class})注释,这意味着仅当在类路径上有DataSource.class和EmbeddedDatabaseType.class类时,才会考虑DataSourceAutoConfiguration中的bean的AutoConfiguration。

    该类还带有@EnableConfigurationProperties(DataSourceProperties.class)批注,该启用了自动将application.properties中的属性绑定到DataSourceProperties类的属性的功能。

     1 @ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
     2 public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {
     3  public static final String PREFIX = "spring.datasource";
     4  ...
     5  ...
     6  private String driverClassName;
     7  private String url;
     8  private String username;
     9  private String password;
    10  ...
    11  //setters and getters
    12 }

    使用此配置,所有以spring.datasource.*开头的属性都将自动绑定到DataSourceProperties对象。

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

    你还可以看到一些内部类和bean定义方法,这些内部类和bean定义方法用SpringBoot的条件注释(例如@ ConditionalOnMissingBean,@ ConditionalOnClass和@ConditionalOnProperty等)进行注释。

    仅当这些条件匹配时,这些Bean定义才会在ApplicationContext中注册。

    你还可以在spring-boot-autoconfigure- {version} .jar中探索许多其他AutoConfiguration类,例如:

    • 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 etc. 

    我希望你现在可以通过使用各种AutoConfiration类以及@Conditional功能来了解Spring Boot自动配置的工作方式。

    感谢阅读!

  • 相关阅读:
    HashMap源码分析jdk1.8
    Struts1.x总结
    session的使用
    浅谈EL
    浅谈JavaBean
    try、catch、finally带return的执行顺序总结
    jvm内存模型
    left join 、right join 、inner join之间的区别
    js按键事件
    log4j配置详解
  • 原文地址:https://www.cnblogs.com/youruike-/p/12361633.html
Copyright © 2020-2023  润新知