看一下 Spring 实现自动配置的原理~~
全部章节传送门:
Spring Boot学习笔记(一):Spring Boot 入门基础
Spring Boot学习笔记(二):Spring Boot 运行原理
Spring Boot学习笔记(三):Spring Boot Web开发
Spring Boot学习笔记(四):Spring Boot 数据访问
Spring Boot学习笔记(五):Spring Boot 企业级开发
Spring Boot学习笔记(六):Spring Boot 应用监控
Spring Boot 启动原理
任何一个 Spring Boot 项目都会有一个启动类,其中有一个 @SpringBootApplication 注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}
其中,比较关键的注解:
- @SpringBootConfiguration:标记当前类为配置类
- @EnableAutoConfiguration:开启自动配置
- @ComponentScan:扫描主类所在的同级包以及下级包里的Bean
在进入核心注解 @EnableAutoConfiguration 的源码中。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
其中的关键部分是 @Import 注解导入的配置功能,AutoConfigurationImportSelector 使用 getCandidateConfigurations 方法得到待配置的class的类名集合。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
SpringFactoriesLoader.loadFactoryNames 方法会扫描 META-INF/spring.factories 文件。
查看 spring-boot-autoconfigure-2.1.3.RELEASE.jar 中的 spring.factories 。
可以看到其中的自动配置。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
...
核心注解
打开任意的 AutoConfiguration 文件(spring-boot-autoconfigure-2.1.3.RELEASE.jar 中),可以看到很多的条件注解,这些注解包含在 org.springframework.boot.autoconfigure.condition 包中,如下所示:
- @ConditionalOnBean:当容器里有指定Bean的条件下
- @ConditionalOnClass:当类路径下有指定的类的条件下
- @ConditionalOnExpression:基于SpEL表达式作为判断条件
- @ConditionalOnJava:基于JVM版本作为判断条件
- @ConditionalOnJndi:在JNDI存在的条件下查找指定的位置
- @ConditionalOnMissingBean:当容器里没有指定Bean的情况下
- @ConditionalOnMissingClass:当容器里没有指定类的情况下
- @ConditionalOnWebApplication:当前项目时Web项目的条件下
- @ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
- @ConditionalOnProperty:指定的属性是否有指定的值
- @ConditionalOnResource:类路径是否有指定的值
- @ConditionalOnOnSingleCandidate:当指定Bean在容器中只有一个,或者有多个但是指定首选的Bean
- @ConditionalOnWebApplication: 当前项目是在 Web 项目的条件下。
这些注解都组合了@Conditional注解,只是使用了不同的条件。
实例分析
下面通过一个简单的 Spring Boot 内置的自动配置: http的编码配置来讲解一下配置流程。
在常规项目中配置 Http 编码的时候是在 web.xml 中配置一个 filter 。
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
自动配置需要满足两个条件:
- 能配置 CharacterEncodingFilter 这个 Bean;
- 能配置 encoding 和 forceEncoding 两个参数。
配置参数的时候使用了在SpringBoot基础章节中讲述的类型安全配置,Spring Boot 也是基于这一点实现的。双击shift全局搜索 HttpProperties(这里需要注意,不是老版本中的 HttpEncodingProperties)。
@ConfigurationProperties(
prefix = "spring.http"
) // 配置前缀
public class HttpProperties {
private boolean logRequestDetails;
private final HttpProperties.Encoding encoding = new HttpProperties.Encoding();
public HttpProperties() {
}
public boolean isLogRequestDetails() {
return this.logRequestDetails;
}
public void setLogRequestDetails(boolean logRequestDetails) {
this.logRequestDetails = logRequestDetails;
}
public HttpProperties.Encoding getEncoding() {
return this.encoding;
}
public static class Encoding {
public static final Charset DEFAULT_CHARSET;
private Charset charset;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;
public Encoding() {
this.charset = DEFAULT_CHARSET;
}
public Charset getCharset() {
return this.charset;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
public boolean isForce() {
return Boolean.TRUE.equals(this.force);
}
public void setForce(boolean force) {
this.force = force;
}
public boolean isForceRequest() {
return Boolean.TRUE.equals(this.forceRequest);
}
public void setForceRequest(boolean forceRequest) {
this.forceRequest = forceRequest;
}
public boolean isForceResponse() {
return Boolean.TRUE.equals(this.forceResponse);
}
public void setForceResponse(boolean forceResponse) {
this.forceResponse = forceResponse;
}
public Map<Locale, Charset> getMapping() {
return this.mapping;
}
public void setMapping(Map<Locale, Charset> mapping) {
this.mapping = mapping;
}
public boolean shouldForce(HttpProperties.Encoding.Type type) {
Boolean force = type != HttpProperties.Encoding.Type.REQUEST ? this.forceResponse : this.forceRequest;
if (force == null) {
force = this.force;
}
if (force == null) {
force = type == HttpProperties.Encoding.Type.REQUEST;
}
return force;
}
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;//默认编码是UTF8
}
public static enum Type {
REQUEST,
RESPONSE;
private Type() {
}
}
}
}
通过调用上述配置,然后根据条件配置 CharacterEncodingFilter 的 Bean 。
@Configuration
@EnableConfigurationProperties({HttpProperties.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
private final Encoding properties;
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
...
}
看上面的注解:
- @Configuration:标明为配置类
- @EnableConfigurationProperties(HttpEncodingProperties.class)声明开启属性注入
- @ConditionalOnClass(CharacterEncodingFilter.class)当CharacterEncodingFilter在类路径的条件下
- @ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true)当spring.http.encoding=enabled的情况下,如果没有设置则默认为true,即条件符合
- @ConditionalOnMissingBean当容器中没有这个Bean时新建Bean 。
实现 starter pom
使用 idea 创建 Maven 的 quickstart 项目,项目信息如下。
<groupId>com.wyk</groupId>
<artifactId>spring-boot-starter-hello</artifactId>
<version>1.0-SNAPSHOT</version>
在 pom.xml 中添加 spring-boot-autoconfigure 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
创建属性配置类,用来获取类型安全的属性。
package com.wyk.springbootstarterhello;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* hello属性配置
*/
@ConfigurationProperties(prefix="hello")
public class HelloServiceProperties {
private static final String MSG = "world";
//添加默认值
private String msg = MSG;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
创建判断依据类。
package com.wyk.springbootstarterhello;
/**
* 判断依据类
*/
public class HelloService {
private String msg;
public String sayHello() {
return "Hello " + msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
创建自动配置类。
package com.wyk.springbootstarterhello;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(HelloServiceProperties.class)
@ConditionalOnClass(HelloService.class)
@ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing=true)
public class HelloServiceAutoConfiguration {
@Autowired
private HelloServiceProperties helloServiceProperties;
@Bean
@ConditionalOnMissingBean
public HelloService helloService() {
HelloService helloService = new HelloService();
helloService.setMsg(helloServiceProperties.getMsg());
return helloService;
}
}
若要配置类生效,还需要注册自动配置类,在src/main/resources 下新建 META-INF/spring.factories,并在其中填写如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wyk.springbootstarterhello.HelloServiceAutoConfiguration
多个配置的话需要使用逗号隔开。
另外,在idea中默认没有resources目录,需要手工创建,然后点击 File->Project Structure...,在弹出的窗口中选择 Modules,然后右击resources目录将其设置为资源。
[图片上传失败...(image-5b5c5b-1553269214786)]
然后需要将项目添加到 Maven 中方便引用。
点击 View->Tool Window->Maven Projects ,会在右侧弹出Maven的工具栏。
[图片上传失败...(image-a6ee64-1553269214786)]
点击Lifecycle中的install即可构建Maven工程并安装到本地仓库,需要清除的话需点击clean 。
接下来创建一个 Spring Boot 的Web项目用来使用前面的 starter 。创建好之后需要添加starter依赖。
<dependency>
<groupId>com.wyk</groupId>
<artifactId>spring-boot-starter-hello</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后简单的修改一下运行类。
@RestController
@SpringBootApplication
public class StatertestdemoApplication {
@Autowired
HelloService helloService;
public static void main(String[] args) {
SpringApplication.run(StatertestdemoApplication.class, args);
}
@RequestMapping("/")
public String index() {
return helloService.sayHello();
}
}
运行程序,访问 http://localhost:8080 ,效果如下图。
在application.properties中添加配置。
hello.msg=wyk
重新运行, 访问 http://localhost:8080 ,效果如下图。