SpringBoot 最重要的功能就是自动配置,帮我们省去繁琐重复地配置工作。相信用过SpringBoot的人,都会被它简洁的步骤所惊讶。那么 SpringBoot 是如何实现自动配置的呢?

在这之前,我们需要了解Spring的@Conditional注解特性,SpringBoot的自动配置魔法正是基于此实现的。

探寻@Conditional的魔力

当开发基于Spring的应用时,我们可能会选择性的注册Bean。

比如说,当程序运行在本地的时候,你可能会注册一个DataSource Bean指向dev数据库。 当程序运行在生产环境时,将DataSource Bean指向一个生产库。

你可以将数据库连接参数抽取到properties文件中,并在恰当的环境中使用这个properties文件。但是当你想连接另外一个数据库环境时,你需要修改properties文件配置。

为了处理这个问题,Spring 3.1提出来Profiles概念。你可以注册多个相同类型的Bean,并用一个或多个profiles文件关联。当你运行程序时,你可以激活需要的profiles文件以及与激活的profiles文件关联的beans,并且只有这些profiles会被激活。


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

通过系统参数 -Dspring.profiles.active=DEV,你可以指定需要激活的profile。

对于简单的情况,比如通过激活的profile开启或者关闭bean的注册,这种方式很好用。但是如果你想通过一些逻辑判断来注册bean,那么这种方式就不那么有效了。

为了更灵活地注册Spring beans,Spring 4提出了@Conditional概念。通过使用@Conditional,你可以根据任何条件选择性地注册bean。

例如,你可以根据下面这些条件来注册bean:

  • 当classpath中存在这个指定的类时
  • 当ApplicationContext中不存在这个指定的Spring bean时
  • 当一个指定的文件存在时
  • 当配置文件中一个指定的属性配置了时
  • 当一个指定的系统属性存在或者不存在时

上面仅仅只是一个很小的例子,你可以制定任何你想要的规则。

我们一起来看看Spring的@Conditional究竟是怎么工作的。

假设我们有一个UserDAO接口,并有一个从数据库获取数据的方法。我们有两个UserDAO接口实现类,一个叫JdbcUserDAO,连接MySQL 数据库。另一个叫MongoUserDAO,连接MongoDB

我们可能启用JdbcUserDAOMongoUserDAO中的一个接口,通过系统属性dbType

如果应用通过java -jar myapp.jar -DdbType=MySQL命令启动,那么将启用JdbcUserDAO。否则,应用通过java -jar myapp.jar -DdbType=MONGO命令启动,那么将启用MongoUserDAO

UserDAOJdbcUserDAOMongoUserDAO的代码如下:

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

实现Condition接口的MySQLDatabaseTypeCondition类,用来检查系统属性dbTypeMySQL,代码如下:

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

同样地,为了检查系统属性dbTypeMongoDBMongoDBDatabaseTypeCondition类的实现如下:

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

现在我们可以通过@Conditional选择性地配置JdbcUserDAOMongoUserDAO。如下:

@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会被注册。如果运行程序类似于java -jar myapp.jar -DdbType=MONGODB这样,那么只有MongoUserDAO会被注册。

到目前为止,我们知道了如何通过System Property属性选择性地注册bean。

假设我们想只有当MongoDB的驱动类"com.mongodb.Server"可以在类路径下获取时,才注册MongoUserDAO,否则注册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的方法。

如果要实现只有Spring中没有UserDAO类型的bean时,才注册MongoUserDAO,要该怎么做?

我们可以创建一个Condition类去检查是否一个指定类型的bean已经存在,具体如下:

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

如果要实现只有当属性app.dbType=MONGO在配置文件中被设置时,才注册MongoUserDAO,要该怎么做?

我们可以这样实现:

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

我们已经举了很多例子去实现条件注册。但是,通过使用注解还有一种更优雅地方式去实现条件注册。我们创建一个 @DatabaseType 注解,而不用为MySQL和MongoDB都实现Condition类,如下:

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

然后我们可以实现DatabaseTypeCondition类,使用 @DatabaseType 的value值来判断是否注册。代码如下:

public class DatabaseTypeCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
Map<String, Object> annotationAttributes = annotatedTypeMetadata
.getAnnotationAttributes(DatabaseType.class.getCanonicalName());
if (annotationAttributes == null) {
return false;
}
String type = (String) annotationAttributes.get("value");
String enabledType = System.getProperty("dbType", "MySQL");
return type != null && type.equalsIgnoreCase(enabledType);
}
}

现在我们可以使用 @DatabaseType 来配置我们的bean,具体如下:

@Configuration
@ComponentScan
public class AppConfig {
@Bean
@DatabaseType("MYSQL")
public UserDAO jdbcUserDAO(){
return new JdbcUserDAOImpl();
}
@Bean
@DatabaseType("MONGO")
public UserDAO mongoUserDAO(){
return new MongoUserDAOImpl();
}
}

这里我们从 @DatabaseType 注解中获取元数据,并与系统属性dbType比较,从而决定是否注册bean。

我们已经通过许多例子来理解如何通过 @Conditional 来控制bean的注册。

SpringBoot中广泛地使用 @Conditional 特性来进行条件注册。

你可以在spring-boot-autoconfigure-{version}.jar的org.springframework.boot.autoconfigure包下找到各种各样的Condition实现类。

至此,我们知道了SpringBoot如何使用 @Conditional 特性来选择性地注册bean,但是自动配置机制是如何触发的呢?

我们接着往下看。

Spring Boot 自动配置

Spring Boot 自动配置魔法的关键是 @EnableAutoConfiguration 注解。

通常我们使用 @SpringBootApplication 来注解一个应用的入口类,也可以使用以下注解来定义:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application{ }

@EnableAutoConfiguration注解能够开启Spring ApplicationContext 的自动配置功能,通过扫描类路径下的组件并注册符合条件的bean。

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

通常,AutoConfiguration 类被 @Configuration 注解,表明这是一个Spring 的配置类。如果被 @EnableConfigurationProperties 注解,则可以绑定自定义的属性。

例如,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration类:

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration { @Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration { } @Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration { } ...
...
}

DataSourceAutoConfiguration上注解着 @ConditionalOnClass({ DataSource.class,EmbeddedDatabaseType.class }) ,说明只有在DataSource.classEmbeddedDatabaseType.class类在类路径下可获得的情况下,自动配置才会生效。

同时,这个类上面还注解着 @EnableConfigurationProperties(DataSourceProperties.class),也就是说它能够自动地将application.properties中的属性绑定到DataSourceProperties类上的属性。

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
...
...
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

你可能还会看到一些其他的条件注解,例如 @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 等等。

一句话总结下来就是,SpringBoot通过 @Conditional 以及各种各样的自动配置类实现SpringBoot的自动配置机制。

SpringBoot自动配置的魔法是怎么实现的的更多相关文章

  1. SpringBoot自动配置的魔法

    Spring自动配置 从@SpringBootApplication注解说起 SpringBoot会根据类路径下的类自动配置,省去了编写繁琐的xml配置文件.原本基于xml配置bean的方式编程基于J ...

  2. SpringBoot自动配置源码调试

    之前对SpringBoot的自动配置原理进行了较为详细的介绍(https://www.cnblogs.com/stm32stm32/p/10560933.html),接下来就对自动配置进行源码调试,探 ...

  3. SpringBoot实战之SpringBoot自动配置原理

    SpringBoot 自动配置主要通过 @EnableAutoConfiguration, @Conditional, @EnableConfigurationProperties 或者 @Confi ...

  4. springboot自动配置源码解析

    springboot版本:2.1.6.RELEASE SpringBoot 自动配置主要通过 @EnableAutoConfiguration, @Conditional, @EnableConfig ...

  5. 源码学习系列之SpringBoot自动配置(篇一)

    源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...

  6. 源码学习系列之SpringBoot自动配置(篇二)

    源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...

  7. Springboot 自动配置浅析

    Introduction 我们知道,SpringBoot之所以强大,就是因为他提供了各种默认的配置,可以让我们在集成各个组件的时候从各种各样的配置文件中解放出来. 拿一个最普通的 web 项目举例.我 ...

  8. SpringBoot自动配置原理学习

    介绍 构建Springboot项目时我们会创建一个启动类 @SpringBootApplication public class DemoApplication { public static voi ...

  9. springboot自动配置国际化失效分析

    最近在整理springBoot国际化时,发现国际化没有生效,通过报错提示在 MessageTag -> doEndTag处打断点 最后发现messageSource并不是ResourceBund ...

随机推荐

  1. 微信小程序实战,与后台交互

    index.wxml <view class="container"> <text>{{txt}}</text> <input name= ...

  2. MySQL引擎类型(三)

    InnoDB: 1)经常更新的表,适合处理多重并发的更新请求. 2)支持事务. 3)可以从灾难中恢复(通过bin-log日志等). 4)外键约束.只有他支持外键. 5)支持自动增加列属性auto_in ...

  3. linux查看文件前几行和后几行的命令

        可以使用head(查看前几行).tail(查看末尾几行)两个命令.例如:查看/etc/profile的前10行内容,应该是:# head -n 10 /etc/profile查看/etc/pr ...

  4. todo---java中的json探讨

    1.json的命名格式 2.json赋值原则 3.json常用的工具 4.json的处理的第三方软件比较 5.json的起源 6.关于json串的对于null ,"" 的不同的第三 ...

  5. 人工智能对人类有哪些影响 选择Python入门怎样

    人工智能对人类有哪些影响?选择Python入门怎样?人工智能是科技时代进步的产物,也是目前人们非常关注的一个产业.那么,随着人工智能的发展,对人类生活的有哪些影响呢? 1.人工智能对文化产业影响 据了 ...

  6. 剑指offer50:数组中重复的数字

    1 题目描述 在一个长度为n的数组里的所有数字都在0到n-1的范围内. 数组中某些数字是重复的,但不知道有几个数字是重复的.也不知道每个数字重复几次.请找出数组中任意一个重复的数字. 例如,如果输入长 ...

  7. Python之推导式笔记

    观察下面的代码: list1 = [] for i in range(10): list1.append(i) print(list1) 作为一个Java出身的程序员,我一定会这么写代码去生成一个列表 ...

  8. idea 控制台 彩色打印日志

    IDEA右上角:Edit Configurations,点击,找到VM options,填入-Dspring.output.ansi.enabled=ALWAYS. 重新启动应用,就会发现控制台日志变 ...

  9. Datatable批量导入到表

    封装批量提交数据到表,用于数据同步作业 private string GetSelectFieldNames(DataTable dataTable, string tableName = " ...

  10. MySQL中You can't specify target table '表名'('sn_app_label') for update in FROM clause错误解决办法

    在有些时候有级联关系的数据放在了同一张表中,在写sql语句的时候可能会遇到这样的场景:我要插入两条数据,第一条是父节点,第二条是子节点,关联关系是父节点的自增长id:在写这样的sql语句时有可能就会出 ...