SpringBoot自动配置原理梳理
近期想梳理一下SpringBoot的自动配置原理,于是决定从SpringBoot相关的配置注解开始梳理,因为这些注解在自动配置中都会用到,所以一步步梳理。
如发现那里有不对的地方希望大佬们指出,我这个小菜鸡继续学习qql。
原创不易,给位需要转载的话可以附上原文链接!!!
1.SpringBoot的特点
-
依赖管理
-
父项目作为依赖
依赖管理 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> 他的父项目 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.4.RELEASE</version> </parent> 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
-
开发导入starter场景启动器
1、见到很多 spring-boot-starter- :就某种场景 2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入 3、SpringBoot所有支持的场景 4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。 5、所有场景启动器最底层的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.5.2.RELEASE</version> <scope>compile</scope> </dependency>
-
无需关注版本号,自动版本仲裁
1、引入依赖默认都可以不写版本 2、引入非版本仲裁的jar,要写版本号。
-
可以修改默认版本号
1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。 2、在当前项目里面重写配置 <properties> <mysql.version>5.1.43</mysql.version> </properties>
-
-
自动配置
-
例如导入了springboot的web的stater的模块里面就会自动配置好tomcat和webmvc的相关模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0mCrMOb-1631089588588)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908111751313.png)]
-
自动配好SpringMVC
-
- 引入SpringMVC全套组件
- 自动配好SpringMVC常用组件(功能)
-
自动配好Web常见功能,如:字符编码问题
-
- SpringBoot帮我们配置好了所有web开发的常见场景
-
默认的包结构
-
- 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
- 无需以前的包扫描配置
-
- 想要改变扫描路径,@SpringBootApplication(scanBasePackages=“com.kele”)
-
- 或者@ComponentScan 指定扫描路径
@SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.kele")
-
各种配置拥有默认值
-
- 默认配置最终都是映射到某个类上,如:MultipartProperties
- 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
-
按需加载所有自动配置项
-
- 非常多的starter
- 引入了哪些场景这个场景的自动配置才会开启
-
- SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
-
…
-
2.SpringBoot的配置注解
-
Configuration+Bean
@Configuration 告诉Spring这是一个配置类 并且也将这个配置类作为组件注册到了容器中 @bean 给容器中配置组件,并且是单实例的
在启动类中可以测试输出一下
MyConfig代码
@Configuration public class MyConfig { @Bean//bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数 public User user01(){ return new User("kele",22,animal()); } @Bean public Pet animal(){ return new Pet("dog"); }
/** * 查看容器中有没有组件 * */ String[] names = run.getBeanDefinitionNames(); MyConfig myConfig = run.getBean(MyConfig.class); System.out.println(myConfig); for (String name : names) { System.out.println("==========="+":"+name); } //看我们注册的组件有没有输出 myConfig user01 animal
-
Configuration里面有需要注意的地方
@Configuration(proxyBeanMethods = true) proxyBeanMethods:代理bean的方法 Full(全量级的配置意思就是说,若容器中存在要调用的对象那么永远都会调用容器里面的) 只要容器中有那么就是一个对象 lite(轻量级的配置意思就是说,不会保存容器中的对象,每次调用都是一个新的) 会自己再创建 如果是全量级别配置,那么User中调用的Pet的依赖就是组件中的就是一个对象 如果是全量级别配置,那么User中调用的Pet就不是容器中的组件了,而是新new了一个 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
-
使用场景
- 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
- 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
-
-
Import注解也可以配置组件
import注解里面声明了一个Class<?>[] value数组,意思就是说可以注册多个类型的组件
@Import({User.class, DBHelper.class}) 代表着注入了一个User类型的组件和一个DBHelper类型的组件
-
Conditional
代表着满足条件就注入
例如
@ConditionalOnMissingBean 如果容器中没有某个bean就注入 相反 @ConditionalOnBean如果容器中有某个bean就注入 @ConditionalOnMissingBean (name = "animal") @Bean //bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数 public User user01(){ /** * 如果是全量级别配置,那么User中嗲用的Pet的依赖就是组件中的就是一个对象 * 如果是全量级别配置,那么User中调用的Pet就不是容器中的组件了,而是新new了一个 */ return new User("kele",22,animal()); } // @Bean public Pet animal(){ return new Pet("dog"); }
-
ImportResource
代表着导入某个资源文件,在项目中之前配置bean使用的是xml的方式现在转为注解的方式很复杂,就在可以使用这个注解,指定资源路径就可以导入配置文件注册组件到容器中
3.配置绑定
-
@ConfigurationProperties
Car.java
使用@Component+@ConfigurationProperties可以将配置文件中的内容和JavaBean进行注册绑定起来 @Component @ConfigurationProperties(prefix = "mycar") @Data public class Car { private String name; private Double price; }
application.properties
mycar.name=Audi mycar.price=100000
-
@EnableConfigurationProperties
@ConfigurationProperties(prefix = “mycar”)+@EnableConfigurationProperties(Car.class)使用这两个注解和JavaBean+配置类已可以做到参数的绑定
Car.java
@ConfigurationProperties(prefix = "mycar") @Data public class Car { private String name; private Double price; }
application.properties
mycar.name=Audi mycar.price=100000
MyConfig.java
@Configuration @EnableConfigurationProperties(Car.class) //@EnableConfigurationProperties(Car.class) 有两个作用:1.开启Car的配置绑定 2. public class MyConfig { @Bean //bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数 public User user01(){ return new User("kele",22,animal()); } @Bean public Pet animal(){ return new Pet("dog"); } }
4.自动配置原理
4.1启动类入口
//从启动类入口进去我们可以可以看到
// @SpringBootApplication这个注解其实就是下面三个注解的结合
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
4.1.1@SpringBootConfiguration
点进去可以看到这个注解里面也就是@Configuration。代表当前是一个配置类
4.1.2@ComponentScan
指定扫描哪些,Spring注解;
4.1.3@EnableAutoConfiguration
//点进去看他也是一个合成注解
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
- @AutoConfigurationPackage
//点进去可以看到
@Import({Registrar.class})
这个Import就是给容器注入组件
//他不是诸如一个他是利用Registrar给容器批量注入组件
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
//点进去可以看到registerBeanDefinitions这个方法的参数,第一个参数代表@AutoConfigurationPackage这注解打在哪里,由于是一个集成注解所以最后这个@AutoConfigurationPackage打在了启动类上所以new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]),这一行代码就是拿到这个注解所打在的类上的包信息,将该包下所有的组件注册到容器里面来
-
@Import({AutoConfigurationImportSelector.class})
已断点的方式看这个AutoConfigurationImportSelector.class怎么走的
点进去在
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9U3EORjK-1631089588590)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908143703416.png)]
看到走到了configurations = this.removeDuplicates(configurations);
可以看到它这个集合的长度是131
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrVm0DEL-1631089588591)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908143942101.png)]
这个131就代表着,SpringBoot启动就要加载的组件个数
1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 步入这个方法可以看到走了这一行代码 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); 我们再点进去SpringFactoriesLoader 看到public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; 主要是这里:"META-INF/spring.factories" 意思就是说会去这个位置去加载文件 3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件 4、从META-INF/spring.factories位置来加载一个文件。 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 spring-boot-autoconfigure-2.5.4.jar包里面也有META-INF/spring.factories
其实这个131个的组件是在spring-boot-autoconfigure-2.5.4.jar这个springboot自动配置jar包中META-INF/spring.factories写死了spring boot已启动需要自动加载的所有配置类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-05lsyGYC-1631089588592)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908144930618.png)]
它肯定不会全部加载所以就有了SpringBoot按需配置
按需开启自动配置项
4.1.4按需开启自动配置项
这里就要用到之前的@Conditional这个注解
虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。
举例说明
-
去spring-boot-autoconfigure-2.5.4这个SpringBoot自动配置类中举例说明里面有很多xxxAutoConfiguration
-
举例aop AopAutoConfiguration
首先在类上我们可以看到
- @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
- @ConditionalOnProperty( prefix = “spring.aop”,name = {“auto”}, havingValue = “true”,matchIfMissing = true) 这个条件匹配的注解就是看配置文件的参数有没有==“spring.aop”==等,matchIfMissing = true这个参数代表就算没有我也认为你有,相当于默认开启
如果类上的判断条件没有过那么类里面的就可以不用看了
在类里面的东西,例如这个ClassProxyingConfiguration内部类
- @Configuration(proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
- @ConditionalOnMissingClass({“org.aspectj.weaver.Advice”})这个条件匹配的意思就是就是看有没有org.aspectj.weaver.Advice这个类,如果没有就生效
- @ConditionalOnProperty(prefix = “spring.aop”,name = {“proxy-target-class”},havingValue = “true”,matchIfMissing = true )这个条件匹配的注解就是看配置文件的参数有没有==“spring.aop”==等,matchIfMissing = true这个参数代表就算没有我也认为你有,相当于默认开启
@Configuration( proxyBeanMethods = false ) @ConditionalOnProperty( prefix = "spring.aop", name = {"auto"}, havingValue = "true", matchIfMissing = true ) public class AopAutoConfiguration { public AopAutoConfiguration() { } @Configuration( proxyBeanMethods = false ) @ConditionalOnMissingClass({"org.aspectj.weaver.Advice"}) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "true", matchIfMissing = true ) static class ClassProxyingConfiguration { ClassProxyingConfiguration() { } @Bean static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() { return (beanFactory) -> { if (beanFactory instanceof BeanDefinitionRegistry) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory; AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } }; } } @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({Advice.class}) static class AspectJAutoProxyingConfiguration { AspectJAutoProxyingConfiguration() { } @Configuration( proxyBeanMethods = false ) @EnableAspectJAutoProxy( proxyTargetClass = true ) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "true", matchIfMissing = true ) static class CglibAutoProxyConfiguration { CglibAutoProxyConfiguration() { } } @Configuration( proxyBeanMethods = false ) @EnableAspectJAutoProxy( proxyTargetClass = false ) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "false" ) static class JdkDynamicAutoProxyConfiguration { JdkDynamicAutoProxyConfiguration() { } } } }
-
举例web模块 DispatcherServletAutoConfiguration
一样的首先看类
- @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
- @ConditionalOnWebApplication(type = Type.SERVLET)这个注解是说web容器是什么类型,spring5.2支持了一个新的技术叫响应式编程也就是WebFlux,我们导入的原生的Web模块并且这里使用的springMVC所以肯定是原生的servlet容器
- @ConditionalOnClass({DispatcherServlet.class})这是条件是有没有DispatcherServlet,既然引入了web依赖那么肯定会有的,因为springboot-web已经集成了webMVC
- 对于@AutoConfigureAfter:在什么之后,@AutoConfigureOrder:配置顺序,这个不做条件
再看内部 DispatcherServletConfiguration
- @Configuration(proxyBeanMethods = false) 代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
- @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class})这个条件就是看里面有没有这个类
- @ConditionalOnClass({ServletRegistration.class})同上
- @EnableConfigurationProperties({WebMvcProperties.class})这个注解就是上面提到的,配置文件绑定
再看这个内部类DispatcherServletConfiguration的内部
- @Bean( name = {“dispatcherServlet”})这个就是注册一个名字叫dispatcherServlet的组件
我们看这个方法他做了什么,它就是new了一个DispatcherServlet对象,并给它的属性进行了初始化值,最后将这个对象返回,这也就证明了,为什么我们没有写springMVC的配置文件但是springMVC是可以用的。
@AutoConfigureOrder(-2147483648) @Configuration( proxyBeanMethods = false ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({DispatcherServlet.class}) @AutoConfigureAfter({ServletWebServerFactoryAutoConfiguration.class}) public class DispatcherServletAutoConfiguration { public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration"; public DispatcherServletAutoConfiguration() { } @Configuration( proxyBeanMethods = false ) @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class}) @ConditionalOnClass({ServletRegistration.class}) @EnableConfigurationProperties({WebMvcProperties.class}) protected static class DispatcherServletConfiguration { protected DispatcherServletConfiguration() { } @Bean( name = {"dispatcherServlet"} ) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; } @Bean @ConditionalOnBean({MultipartResolver.class}) @ConditionalOnMissingBean( name = {"multipartResolver"} ) public MultipartResolver multipartResolver(MultipartResolver resolver) { return resolver; } } }
4.1.5修改默认配置
以web模块中DispatcherServlet中的MultipartResolver文件上传解析器为例,这个东西非常有趣
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
//SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
给容器中加入了文件上传解析器;
5.SpringBoot配置文件生效的原理
我们在springboot中经常去application.properties中配置一些属性,那么这些配置文件是怎么生效的呢,我现在以字符编码解析器为例
-
HttpEncodingAutoConfiguration 以这个自动配置类分析
这个只需要看一些关键点就行
同样的先分析类上的注解
- @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
- @ConditionalOnWebApplication(type = Type.SERVLET)这个注解是说web容器是什么类型,spring5.2支持了一个新的技术叫响应式编程也就是WebFlux,我们导入的原生的Web模块并且这里使用的springMVC所以肯定是原生的servlet容器
- @EnableConfigurationProperties({ServerProperties.class})这个注解可以将配置文件中的内容和JavaBean进行注册绑定起来@ConditionalOnClass({CharacterEncodingFilter.class})这个条件判断就是看有没有CharacterEncodingFilter这个类
- @ConditionalOnProperty(prefix = “server.servlet.encoding”, value = {“enabled”},matchIfMissing = true)
看到ServerProperties.class可以看到配置文件跟这个类绑定了,证明这个类的属性就是我们在配置文件设置的属性,具体头部是怎么开始的呢
我们看到这里prefix = “server.servlet.encoding”,证明我们在配置文件中以这个开始,设置属性就是ServerProperties.class中的属性名称
这样就做到了我们修改的配置文件,springboot是如何生效的
-
还有个注意点
@ConditionalOnMissingBean这个条件如果没有配置组件,那么SpringBoot才会给你配置
所以应证了一句话:SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
//SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先 @Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() {}
@Configuration( proxyBeanMethods = false ) @EnableConfigurationProperties({ServerProperties.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({CharacterEncodingFilter.class}) @ConditionalOnProperty( prefix = "server.servlet.encoding", value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration { private final Encoding properties; public HttpEncodingAutoConfiguration(ServerProperties properties) { this.properties = properties.getServlet().getEncoding(); } @Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE)); return filter; }
总结
这里总结我感觉我总结的不是很好,于是引用了雷丰阳老师的总结,我觉得总结非常到位
-
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
-
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
定制化配置
-
- 用户直接自己@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties