Spring Boot 自动配置原理及自定义Starter
一、Spring Boot 是怎么完成自动配置的?
初识Spring Boot时我们就知道,Spring Boot有一个全局配置文件:application.properties或application.yml。这些配置是如何在Spring Boot项目中生效的呢?
Spring Boot关于自动配置的源码在spring-boot-autoconfigure-x.x.x.x.jar中:
自动配置始于Spring Boot的核心注解:@SpringBootApplication ,它在主函数所在的类中,是个复合注解:
这里面 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan 是比较重要的注解,但 @EnableAutoConfiguration 是和自动配置关系最紧密的注解。
@EnableAutoConfiguration 也是一个复合注解:
@EnableAutoConfiguration 的关键功能由@Import提供,原因是它导入了一个很重要的类:AutoConfigurationImportSelector,这个类提供了一个重要的方法 selectImports():
selectImports()中 getCandidateConfigurations()是个重要的方法,它通过SpringFactoriesLoader.loadFactoryNames()扫描所有META-INF/spring.factories具有的jar包:
spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件:
这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些xxxxAutoConfiguration就是自动配置类,比如这个文件中可以找到RedisAutoConfiguration、RabbitAutoConfiguration等自动配置类。
Spring Boot启动类(main方法所在类)在启动的时候,就会执行run(...)方法,run()方法的内部就会执行selectImports()方法把所有的自动配置类加载到Spring容器。
每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项(在autoconfigure.condition包下还有很多,下面简单列举几个):
- @ConditionalOnBean:当容器里有指定的bean的条件下。
- @ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
- @ConditionalOnClass:当类路径下有指定类的条件下。
- @ConditionalOnMissingClass:当类路径下不存在指定类的条件下。
- @ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。
application.yml文件中的配置是怎么生效的?比如 spring.rabbitmq.port=5672 这个配置项是怎么生效的,我们可以看下RabbitAutoConfiguration类:
在RabbitAutoConfiguration上面有个 @EnableConfigurationProperties 注解,它的作用是开启配置属性,让 @ConfigurationProperties 注解的properties类生效并使用。如让RabbitProperties生效并注册到Spring容器中,并且使用!
上面 @EnableConfigurationProperties 注解后面有一个 RabbitProperties 类:
RabbitProperties类上面就是由 @ConfigurationProperties 注解加持的,@ConfigurationProperties 和 @EnableConfigurationProperties 结合使用,@ConfigurationProperties的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。在全局配置的属性如:spring.rabbitmq.port=5672 等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。
而诸多的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring 容器导入bean,而所有导入的bean所需要的属性都通过xxxxProperties的bean来获得。
回头再看RabbitAutoConfiguration类就能明白上面的这句话的含义,如下RabbitAutoConfiguration会在Spring容器中返回template这个Bean,这个Bean的属性已经从RabbitProperties的bean中获得(RabbitProperties的bean的属性通过@ConfigurationProperties 注解由application.yml文件中取得),但是仔细看template这个bean的上面还有一个@ConditionalOnMissingBean 注解,即当容器里不存在指定bean的条件下这个自动配置才会生效,注册该bean到spring容器,@ConditionalXxx 和 全局配置文件application.yml的结合可以实现灵活的配置。
总结:
- XxxxProperties类的含义是:封装配置文件中相关属性;XxxxAutoConfiguration类的含义是:自动配置类,目的是给容器中添加组件。而其他的主方法启动,则是为了加载这些五花八门的XxxxAutoConfiguration类。
-
如何用好自动配置:1)SpringBoot启动会加载大量的自动配置类2)我们看我们需要的功能有没有SpringBoot默认写好的自动配置类;3)我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了)4)给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值。
二、Spring Boot 自动配置原理给我们的启示
上面的自动配置过程是搞明白了,但是这个能够给我们带来什么呢?
那就是我们也可以仿照这个自动配置过程完成自己的starter并实现自动配置。
2.1 Spring Boot中的 Starter
Spring Boot 的便利性体现在,它简化了很多烦琐的配置,这对于开发人员来说是一个福音,通过引入各种 Spring Boot Starter 包可以快速搭建出一个项目的脚手架。目前提供的 Spring Boot Starter 包有:
- spring-boot-starter-web:快速构建基于 Spring MVC 的 Web 项目,使用 Tomcat 做默认嵌入式容器。
- spring-boot-starter-data-redis:操作 Redis。
- spring-boot-starter-data-mongodb:操作 Mongodb。
- spring-boot-starter-data-jpa:操作 Mysql。
- spring-boot-starter-activemq:操作 Activemq。
- ……
自动配置非常方便,当我们要操作 Mongodb 的时候,只需要引入 spring-boot-starter-data-mongodb 的依赖,然后配置 Mongodb 的链接信息 spring.data.mongodb.uri=mongodb://localhost/test 就可以使用 MongoTemplate 来操作数据,MongoTemplate 的初始化工作全部交给 Starter 来完成。
结合上面的自动配置过程我们也可以实现自己的 Starter:
(1)创建一个项目 spring-boot-starter-demo,pom.xml 配置代码如下
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
(2)创建一个配置类,用于在属性文件中配置值,相当于 RabbitProperties 类,代码如下所示:
@Data
@ConfigurationProperties("spring.user")
public class UserPorperties {
private String name;
}
通过 @ConfigurationProperties("spring.user") 注解可以绑定application.yml配置文件中 spring.rabbitmq.port=5672 这种形式的配置,这里"spring.user"就是会识别spring.user.XXX这样的配置。
(3)再定义一个 Client,相当于 RabbitTemplate,里面定一个方法,用于获取配置中的值,代码如下所示:
public class UserClient {
private UserPorperties userPorperties;
public UserClient() {
}
public UserClient(UserPorperties p) {
this.userPorperties = p;
}
public String getName() {
return userPorperties.getName();
}
}
以上,一个基本的 Starter 就已经创建好了。
(4)一个最基本的 Starter 包定义好了,但目前肯定是不能使用 UserClient,因为我们没有自动构建 UserClient 的实例。接下来开始构建 UserClient,代码如下所示:
@Configuration
@EnableConfigurationProperties(UserPorperties.class)
public class UserAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = "spring.user", value = "enabled", havingValue = "true")
public UserClient userClient(UserPorperties userPorperties) {
return new UserClient(userPorperties);
}
}
@ConfigurationProperties的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中。
(5)在 resources /META-INF/spring.factories 文件中添加自动配置类UserAutoConfiguration:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.cxytiandi.demo.UserAutoConfigure
Spring Boot 会默认扫描跟启动类平级的包,假如我们的 Starter 跟启动类不在同一个主包下,通过在spring.factories 文件中添加自动配置类的方式,Spring Boot 启动时会去读取 spring.factories 文件,然后根据配置激活对应的配置类,至此一个简单的 Starter 包就实现了。
(6)使用 Starter
现在可以在其他的项目中引入这个 Starter 包,如下:
<dependency>
<groupId>com.cxytiandi</groupId>
<artifactId>spring-boot-starter-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
引入之后就直接可以使用 UserClient,UserClient 在项目启动的时候已经自动初始化好:
@RestController
public class UserController {
@Autowired
private UserClient userClient;
@GetMapping("/user/name")
public String getUserName() {
return userClient.getName();
}
}
在属性文件中配置 name 的值和开启 UserClient:
spring.user.name=zhangsan
spring.user.enabled=true
访问 /user/name 就可以返回我们配置的 zhangsan。
三、使用注解开启 Starter 自动构建
很多时候我们不想在引入 Starter 包时就执行初始化的逻辑,而是想要由用户来指定是否要开启 Starter 包的自动配置功能,比如常用的 @EnableAsync 这个注解就是用于开启调用方法异步执行的功能。
同样地,我们也可以通过注解的方式来开启是否自动配置,如果用注解的方式,那么 spring.factories 就不需要编写了,下面就来看一下怎么定义启用自动配置的注解,代码如下所示:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({UserAutoConfigure.class})
public @interface EnableUserClient {
}
这段代码的核心是 @Import({UserAutoConfigure.class}),通过导入的方式实现把 UserAutoConfigure 实例加入 SpringIOC 容器中,这样就能开启自动配置了。使用方式就是在启动类上加上该注解,代码如下所示:
@SpringBootApplication
@EnableUserClient
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
在某些场景下,UserAutoConfigure 中会配置多个对象,对于这些对象,如果不想全部配置,或是想让用户指定需要开启配置的时候再去构建对象,这个时候我们可以通过 @ConditionalOnProperty 来指定是否开启配置的功能,代码如下所示:
@Bean
@ConditionalOnProperty(prefix = "spring.user",value = "enabled",havingValue = "true")
public UserClient userClient(UserPorperties userPorperties) {
return new UserClient(userPorperties);
}
通过上面的配置,只有当启动类加了 @EnableUserClient 并且配置文件中 spring.user.enabled=true 的时候才会自动配置 UserClient。
四、JavaConfig
Spring JavaConfifig 是 Spring 社区的产品,Spring 3.0引入了他,它提供了配置 Spring IOC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。
使用 JavaConfifig 的优点在于:
面向对象的配置。由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean 方法等。 减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。
但是,许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfifig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfifig 配置类来配置容器是可行的,但实际上很多人认为将JavaConfifig 与 XML 混合匹配是理想的。类型安全和重构友好。JavaConfifig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。
常用的Java config:
- @Confifiguration:在类上打上写下此注解,表示这个类是配置类
- @ComponentScan:在配置类上添加 @ComponentScan 注解。该注解默认会扫描该类所在的包下所有的配置类,相当于之前的 <context:component-scan >。
- @Bean:bean的注入:相当于以前的< bean id="objectMapper" class="org.codehaus.jackson.map.ObjectMapper" />
- @EnableWebMvc:相当于xml的<mvc:annotation-driven >
- @ImportResource: 相当于xml的 < import resource="applicationContext cache.xml">