SpringBoot框架—自动配置原理的解密
什么是SpringBoot的自动配置?
SpringBoot官网对Spring Boot的定义如下:
Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can run. We take an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.
用一句话来概括这段话的核心思想,就是“约定优于配置”。也就是说,我们在开发基于Spring的应用时,SpringBoot框架会帮我们做很多默认的配置,这样大大地减少了我们的工作量。以extensible项目(SpringBoot版本:2.0.4.RELEASE)为例,现在我们来看看SpringBoot框架是如何进行自动配置的。
SpringBoot自动配置相关源码解读
所有SpringBoot项目主函数入口的类上都有一个组合注解@SpringBootApplication,而这个注解类上有一个注解@EnableAutoConfiguration。望文知意,这个注解就是用来实现自动配置的,其源码如下:
package org.springframework.boot.autoconfigure;
@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.class}) AutoConfigurationImportSelector这个类会导入所有需要自动配置的类,其关键代码如下:
package org.springframework.boot.autoconfigure;
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
// other codes...
/**
* 对SpringBoot默认的自动配置类列表进行处理,筛选出需要SpringBoot进行自动配置类的列表
*/
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return StringUtils.toStringArray(configurations);
}
/**
* 判断是否需要进行自动配置
*/
protected boolean isEnabled(AnnotationMetadata metadata) {
return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
}
/**
* 加载SpringBoot默认的自动配置类列表
*/
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;
}
// other codes...
}
selectImports()方法的作用:筛选出需要SpringBoot自动配置的配置类列表,并进行相应的配置。
isEnalbed()方法的作用:加载porperties配置文件,判断该项目是否开启了自动配置功能,默认就是开启状态。
getCandidateConfigurations()方法的作用: 加载META-INF/spring.factories中的信息,获取SpringBoot默认的自动配置类列表。
web项目发送http请求可能会出现乱码,以前为了解决这个问题,我们是在web.xml添加一个filter,统一强制设置http的编码为utf-8。如下:
<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>
接下来看SpringBoot是如何帮我们自动配置Http编码的。刚刚的spring.factories里面有内容如下:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
# other codes...
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
# other codes...
HttpEncodingAutoConfiguration配置类的关键代码如下:
package org.springframework.boot.autoconfigure.web.servlet;
@Configuration
@EnableConfigurationProperties({HttpEncodingProperties.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
private final HttpEncodingProperties properties;
public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
this.properties = properties;
}
@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.HttpEncodingProperties.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpEncodingProperties.Type.RESPONSE));
return filter;
}
// other codes...
}
这个类的作用是:如果在properties配置文件中没有设置对应的属性(spring.http.encoding)的值时,SpringBoot会创建一个过滤器CharacterEncodingFilter,并会强制设置其编码—filter.setEncoding(this.properties.getCharset().name());
而HttpEncodingProperties的关键源码如下:
package org.springframework.boot.autoconfigure.http;
@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties {
public static final Charset DEFAULT_CHARSET;
private Charset charset;
public HttpEncodingProperties() {
this.charset = DEFAULT_CHARSET;
}
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;
}
// other codes...
}
这里是@EnableConfigurationProperties/@ConditionalOn***/@ConfigurationProperties,这三个注解协同作用,实现自动配置—给属性赋默认值。
其他注解的功能很好理解,就是@ConditionOn***注解不好理解,而且种类繁多。
以@ConditionalOnClass为例,进行源码解读:
package org.springframework.boot.autoconfigure.condition;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnClassCondition.class})
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
@Conditional相关的代码如下:
package org.springframework.context.annotation;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
package org.springframework.context.annotation;
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
说明:
1. @ConditionalOnClass注解的类上面有注解@Conditional({OnClassCondition.class})
2. OnClassCondition.class必定是Condition.class的子类,而且应该有matches()方法的实现—这里有个地方需要额外注意,matches()方法的实现并不是发生在OnClassCondition里面,而是发生在抽象类SpringBootCondition里面。
3. 类似的,其他的On***Condition.class都是Condition.class的子类,都继承了SpringBootCondition。
SpringBootCondition的关键代码如下:
package org.springframework.boot.autoconfigure.condition;
public abstract class SpringBootCondition implements Condition {
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
this.logOutcome(classOrMethodName, outcome);
this.recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
} catch (NoClassDefFoundError var5) {
throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
} catch (RuntimeException var6) {
throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
}
}
public abstract ConditionOutcome getMatchOutcome(ConditionContext var1, AnnotatedTypeMetadata var2);
// other codes...
}
SpringBootCondition.matches()方法是用final修饰的,所以不可被子类重写。
其他的代码都是记录作用,与实际功能关系不大。
这里最关键的一句代码是:ConditionOutcome outcome = this.getMatchOutcome(context, metadata); 根据JAVA语言多态的特性,需要知道On***Condition.class具体作用的话,就去查看其getMatchOutcome()方法是如何实现的!!!Spring提供的辅助自动配置用的注解有如下:
汇总这些注解(对应的类)的作用如下:
注解 | 功能 |
---|---|
@ConditionalOnBean | 当SpringIoc容器内存在指定Bean的条件,才会实例化Bean |
@ConditionalOnClass | 当SpringIoc容器内存在指定Class的条件,才会实例化Bean |
@ConditionalOnCloudPlatform | 云平台与给定参数一致时,才会实例化Bean |
@ConditionalOnExpression | 当表达式为true的时,才会实例化Bean |
@ConditionalOnJava | 当前java版本在给定参数的范围内时,才会实例化Bean |
@ConditionalOnJndi | 在JNDI存在时,才会实例化Bean |
@ConditionalOnMissingBean | 在当前上下文中不存在某个对象时,才会实例化Bean |
@ConditionalOnMissingClass | 某个class类路径上不存在的时候,才会实例化Bean |
@ConditionalOnNotWebApplication | 当前项目不是Web项目,才会实例化Bean |
@ConditionalOnProperty | 指定的属性有指定的值,才会实例化Bean |
@ConditionalOnResource | 类路径有指定的值,才会实例化Bean |
@ConditionalOnSingleCandidate | 当指定Bean在SpringIoc容器内只有一个,或者虽然有多个但是是指定首选的Bean,才会实例化Bean |
@ConditionalOnWebApplication | 当前项目是Web项目,才会实例化Bean |
实战—实现一个能够具备自动配置功能的模块
新建一个maven项目robot如下:
在该项目中导入自动配置必要的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
导入成功后的效果图如下:
定义配置类RobotProperties
package com.netopstec.robot;
/**
* robot项目的配置
* @author zhenye 2018/8/27
*/
@ConfigurationProperties(prefix = "robot")
public class RobotProperties {
private static final String DEFAULT_NAME = "Big White";
private static final String DEFAULT_AGE = "7";
/**
* 如果不配置(robot.name/age)的话,就是默认值。
*/
private String name = DEFAULT_NAME;
private String age = DEFAULT_AGE;
// ...getters and setters
}
定义业务类RobotService
package com.netopstec.robot;
/**
* 提供给外部系统的业务类
* @author zhenye 2018/8/27
*/
public class RobotService {
private String name;
private String age;
/**
* 提供给外部系统的方法
* @return
*/
public String selfIntroduction(){
return "If you don't set something in properties, then robot's name is " + name + " and robot's age is " + age + ".";
}
// ...getters and setters
}
定义自动配置的实现类RobotConfiguration
/**
* 注解@EnableConfigurationProperties指定自动配置的类
* 注解@ConditionalOnProperty开启自动配置的实现
* @author zhenye 2018/8/27
*/
@Configuration
@EnableConfigurationProperties(RobotProperties.class)
@ConditionalOnClass(RobotService.class)
@ConditionalOnProperty(prefix = "robot",value = "enabled",matchIfMissing = true)
public class RobotConfiguration {
@Autowired
private RobotProperties robotProperties;
@Bean
@ConditionalOnMissingBean(RobotService.class)
public RobotService robotService(){
RobotService robotService = new RobotService();
robotService.setName(robotProperties.getName());
robotService.setAge(robotProperties.getAge());
return robotService;
}
}
在resources下面新建META-INF文件夹和spring.factories(加载自动配置类),并编辑内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.netopstec.robot.RobotConfiguration
具体的robot项目结构图如下:
在命令行工具中运行命令”mvn install”,将robot项目打包发布到maven的本地仓库中。其打包发布的日志如下(BUILD SUCCESS说明打包发布成功,pom文件中红色框标注的是该项目的maven坐标):
要想使用这个robot项目的自动配置功能,只需要在pom文件中加入robot项目的maven坐标,然后通过注解@Autowired导入RobotService就行。
robot项目的maven坐标:
测试类的代码如下:
package com.netopstec.extensible.controller;
/**
* @author zhenye 2018/8/27
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private RobotService robotService;
@RequestMapping("/robot")
public String test(){
return robotService.selfIntroduction();
}
}
启动项目,在Postman中测试,发现导入了自动配置的内容。
自定义robot属性值如下:
重新启动项目,再次在Postman中测试,效果图如下: