SpringBoot核心流程
创建springbootApplication
1.推测应用类型,正常是SERVLET
2.获取所有的初始化器
3.获取所有的监听器
4.推断main主类
run方法
1.运行监听器
2.加载配置文件,运行时环境参数
3.打印banner条
4.加载应用上下文(Tomcat在此创建)
5.加载Spring容器(refreshContext方法,Spring容器)
SpringBoot核心前置内容
1.Spring注解编程的发展过程
1.1 Spring 1.x
2004年3月24日,Spring1.0 正式发布,提供了IoC,AOP及XML配置的方式。
在Spring1.x版本中提供的是纯XML配置的方式,也就是在该版本中必须要提供xml的配置文件,在该文件中通过 <bean>
标签来配置需要被IoC容器管理的Bean。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.bobo.demo01.UserService" />
</beans>
调试代码
public static void main(String[] args) {
ApplicationContext ac = new FileSystemXmlApplicationContext("classpath:applicationContext01.xml");
System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
输出结果
在Spring1.2版本的时候提供了@Transaction (org.springframework.transaction.annotation )注解。简化了事务的操作.
1.2 Spring 2.x
在2006年10月3日 Spring2.0问世了,在2.x版本中,比较重要的特点是增加了很多注解
Spring 2.5之前
在2.5版本之前新增的有 @Required
@Repository
@Aspect
,同时也扩展了XML的配置能力,提供了第三方的扩展标签,比如 <dubbo>
@Required
如果你在某个java类的某个set方法上使用了该注释,那么该set方法对应的属性在xml配置文件中必须被设置,否则就会报错!!!
public class UserService {
private String userName;
public String getUserName() {
return userName;
}
@Required
public void setUserName(String userName) {
this.userName = userName;
}
}
如果在xml文件中不设置对应的属性就会给出错误的提示。
设置好属性后就没有了错误提示了
源码中可以看到 @Required
从2.0开始提供
@Repository
@Repository 对应数据访问层Bean.这个注解在Spring2.0版本就提供的有哦,大家可能没有想到。
@Aspect
@Aspect是AOP相关的一个注解,用来标识配置类。
Spring2.5 之后
在2007年11月19日,Spring更新到了2.5版本,新增了很多常用注解,大大的简化配置操作。
注解 | 说明 |
---|---|
@Autowired | 依赖注入 |
@Qualifier | 配置@Autowired注解使用 |
@Component | 声明组件 |
@Service | 声明业务层组件 |
@Controller | 声明控制层组件 |
@RequestMapping | 声明请求对应的处理方法 |
在这些注解的作用下,可以不用在xml文件中去注册没有bean,这时只需要指定扫码路径,然后在对应的Bean头部添加相关的注解即可,这大大的简化了配置及维护工作。案例如下:
在配置文件中只需要配置扫码路径即可:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.bobo" />
</beans>
持久层代码:
@Repository
public class UserDao {
public void query(){
System.out.println("dao query ..." );
}
}
业务逻辑层代码
@Service
public class UserService {
@Autowired
private UserDao dao;
public void query(){
dao.query();
}
}
控制层代码:
@Controller
public class UserController {
@Autowired
private UserService service;
public void query(){
service.query();
}
}
测试代码
public class Demo02Main {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext02.xml");
UserController acBean = ac.getBean(UserController.class);
acBean.query();
}
}
虽然在Spring的2.5版本提供了很多的注解,也大大的简化了开发,但是任然没有摆脱XML配置驱动。
1.3 Spring 3.x
在2009年12月16日发布了Spring3.0版本,这是一个注解编程发展的里程碑版本,在该版本中全面拥抱Java5。提供了 @Configuration
注解,目的就是去xml化。同时通过 @ImportResource
来实现Java配置类和XML配置的混合使用来实现平稳过渡。
/**
* @Configuration 标注的Java类 相当于 application.xml 配置文件
*/
@Configuration
public class JavaConfig {
/**
* @Bean 注解 标注的方法就相当于 <bean></bean> 标签
也是 Spring3.0 提供的注解
* @return
*/
@Bean
public UserService userService(){
return new UserService();
}
}
在Spring3.1 版之前配置扫描路径还只能在 XML 配置文件中通过 component-scan
标签来实现,在3.1之前还不能够完全实现去XML配置,在3.1 版本到来的时候,提供了一个 @ComponentScan
注解,该注解的作用是替换掉 component-scan
标签,是注解编程很大的进步,也是Spring实现无配置话的坚实基础。
@ComponentScan
@ComponentScan的作用是指定扫码路径,用来替代在XML中的 <component-scan>
标签,默认的扫码路径是当前注解标注的类所在的包及其子包。
定义UserService
@Service
public class UserService {
}
创建对于的Java配置类
@Configuration
@ComponentScan
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
}
输出的结果
当然也可以指定特定的扫描路径
@Configuration
// 指定特定的扫描路径
@ComponentScan(value = {"com.bobo.demo04"})
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
}
@Import
@Import注解只能用在类上,作用是快速的将实例导入到Spring的IoC容器中,将实例导入到IoC容器中的方式有很多种,比如 @Bean
注解,@Import注解可以用于导入第三方包。具体的使用方式有三种。
(用在类上,也能将类导入spring容器
)
静态导入
静态导入的方式是直接将需要导入到IoC容器中的对象类型直接添加进去即可。
这种方式的好处是简单,直接,但是缺点是如果要导入的比较多,则不太方便,而且也不灵活。
ImportSelector
@Import
注解中也可以添加一个实现了 ImportSelector
接口的类型,这时不会将该类型导入IOC容器中,而是会调用 ImportSelector
接口中定义的 selectImports
方法,将该方法的返回的字符串数组的类型添加到容器中
。
定义两个业务类
public class Cache {
}
public class Logger {
}
定义ImportSelector接口的实现,方法返回的是需要添加到ICC容器中的对象对应的类型的全类路径的字符串数组,可以根据不同的业务需求而导入不同的类型,会更加的灵活些
。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Logger.class.getName(),Cache.class.getName()};
}
}
导入测试案例
@Configuration
@Import(MyImportSelector.class)
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}
输出结果:
ImportBeanDefinitionRegistrar
除了上面所介绍的ImportSelector方式灵活导入以外还提供了 ImportBeanDefinitionRegistrar
接口,也可以实现,相比 ImportSelector
接口的方式,ImportBeanDefinitionRegistrar 的方式是直接在定义的方法中提供了 BeanDefinitionRegistry
,自己在方法中实现注册
。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 将需要注册的对象封装为 RootBeanDefinition 对象
RootBeanDefinition cache = new RootBeanDefinition(Cache.class);
registry.registerBeanDefinition("cache",cache);
RootBeanDefinition logger = new RootBeanDefinition(Logger.class);
registry.registerBeanDefinition("logger",logger);
}
}
测试代码
@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}
输出结果
@EnableXXX
@Enable模块驱动,其实是在系统中先开发好各个功能独立的模块,比如 Web MVC 模块, AspectJ代理模块,Caching模块等。
案例说明,先定义好功能模块
/**
* 定义一个Java配置类
*/
@Configuration
public class HelloWorldConfiguration {
@Bean
public String helloWorld(){
return "Hello World";
}
}
然后定义@Enable注解
/**
* 定义@Enable注解
* 在该注解中通过 @Import 注解导入自定义的模块,使之生效。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloWorldConfiguration.class)
public @interface EnableHelloWorld {
}
测试代码
@Configuration
// 加载 自定义 模块
@EnableHelloWorld
public class JavaMian {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaMian.class);
String helloWorld = ac.getBean("helloWorld", String.class);
System.out.println("helloWorld = " + helloWorld);
}
}
效果
1.4 Spring 4.x
2013年11月1 日更新的Spring 4.0 ,完全支持Java8.这是一个注解完善的时代,提供的核心注解是@Conditional条件注解
。@Conditional 注解的作用是按照一定的条件进行判断,满足条件就给容器注册Bean实例。
@Conditional的定义为:
// 该注解可以在 类和方法中使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* 注解中添加的类型必须是 实现了 Condition 接口的类型
*/
Class<? extends Condition>[] value();
}
Condition是个接口,需要实现matches方法,返回true则注入bean,false则不注入。
案例讲解:
/**
* 定义一个 Condition 接口的是实现
*/
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
@verride
public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata) {
// 根据特定的业务需求来决定是否注入对应的对象
try {
boolean flage = context.getRegistry().containsBeanDefinition("userService");
if (flage) {
Class.forName("com.bobo.test.test666");
return flage;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return false;// 默认返回false
}
}
创建Java配置类
@Configuration
public class JavaConfig {
@Bean
// 条件注解,添加的类型必须是 实现了 Condition 接口的类型
// MyCondition的 matches 方法返回true 则注入,返回false 则不注入
@Conditional(MyCondition.class)
public StudentService studentService(){
return new StudentService();
}
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}
测试:
但是将 matchs方法的返回结果设置为 true 则效果不同
所以@Conditional的作用就是提供了对象导入IoC容器的条件机制,这也是SpringBoot中的自动装配的核心关键。当然在4.x还提供一些其他的注解支持,比如 @EventListener
,作为ApplicationListener接口编程的第二选择,@AliasFor
解除注解派生的时候冲突限制。@CrossOrigin
作为浏览器跨域资源的解决方案。
1.5 Spring 5.x
2017年9月28日,Spring来到了5.0版本。5.0同时也是SpringBoot2.0的底层。注解驱动的性能提升方面不是很明显。在Spring Boot应用场景中,大量使用@ComponentScan扫描,导致Spring模式的注解解析时间耗时增大,因此,5.0时代引入**@Indexed**,为Spring模式注解添加索引
。
当在项目中使用了 @Indexed
之后,编译打包的时候会在项目中自动生成 META-INT/spring.components
文件。当Spring应用上下文执行 ComponentScan
扫描时,META-INT/spring.components
将会被 CandidateComponentsIndexLoader
读取并加载,转换为 CandidateComponentsIndex
对象,这样的话 @ComponentScan
不在扫描指定的package,而是读取 CandidateComponentsIndex
对象,从而达到提升性能的目的。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
</dependency>
使用@Indexed注解
核心条件注解@Conditional控制对象是否注入到容器中,@Import 注解中导入的类型
ImportSelector 接口,不会将该类型注入到容器中而是会将selectImports的返回的类型的全类路径的字符串的数据注入到容器中动态注入(类似于factoryBean)
ImportBeanDefinitionRegistrar 的方式是直接在定义的方法中提供了 BeanDefinitionRegistry
,自己在方法中实现注册
。(自己注册了bean定义,接口类似于factoryBean)
@Indexed ,编译打包的时候会在项目中自动生成 META-INT/spring.components
文件。当Spring应用上下文执行 ComponentScan
扫描时,META-INT/spring.components
将会被 CandidateComponentsIndexLoader
读取并加载,从而达到提升spring性能的目的。
2. 什么是SPI
为什么要讲SPI呢?因为在SpringBoot的自动装配中其实有使用到SPI机制,所以掌握了这部分对于SpringBoot的学习还是很有帮助的。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。先通过一个很简单的例子来看下它是怎么用的。
案例介绍
先定义接口项目
然后创建一个扩展的实现,先导入上面接口项目的依赖
<dependencies>
<dependency>
<groupId>com.bobo</groupId>
<artifactId>JavaSPIBase</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
然后创建接口的实现
/**
* SPI:MySQL对于 baseURL 的一种实现
*/
public class MySqlConnection implements BaseData {
@Override
public void baseURL() {
System.out.println("mysql 的扩展实现....");
}
}
然后在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称
。然后在文件中写上接口的实现类的全类路径名称
。
同样的再创建一个案例
然后在测试的项目中测试
public static void main(String[] args) {
ServiceLoader<BaseData> providers = ServiceLoader.load(BaseData.class);
Iterator<BaseData> iterator = providers.iterator();
while(iterator.hasNext()){
BaseData next = iterator.next();
next.baseURL();
}
}
根据不同的导入,执行的逻辑会有不同
源码查看
ServiceLoader
首先来看下ServiceLoader的类结构
// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务 类或者接口
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// 访问权限的上下文对象
private final AccessControlContext acc;
// 保存已经加载的服务类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 内部类,真正加载服务类
private LazyIterator lookupIterator;
load
load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。
public final class ServiceLoader<S> implements Iterable<S>
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//要加载的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
//先清空
providers.clear();
//实例化内部类
LazyIterator lookupIterator = new LazyIterator(service, loader);
}
}
查找实现类和创建实现类的过程,都在LazyIterator完成。当调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
//第二次调用的时候,已经解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
//META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件对象,读取内容,最后返回
pending = parse(service, configs.nextElement());
}
//拿到第一个实现类的类名
nextName = pending.next();
return true;
}
}
创建实例对象,当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。
private class LazyIterator implements Iterator<S>{
private S nextService() {
//全限定类名
String cn = nextName;
nextName = null;
//创建类的Class对象
Class<?> c = Class.forName(cn, false, loader);
//通过newInstance实例化
S p = service.cast(c.newInstance());
//放入集合,返回实例
providers.put(cn, p);
return p;
}
}
看到这儿,已经很清楚了。获取到类的实例,自然就可以对它为所欲为了!
SpringBoot自动装配原理分析
自动装配源码分析
在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配。所以,所谓的自动装配,实际上就是如何自动将bean装载到Ioc容器中来
。
实际上在spring 3.x版本中,Enable模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在spirng 4.x版本中,conditional条件注解的出现。看一下spring boot的自动装配是怎么一回事。
ImportSelector
接口,调用 ImportSelector
接口中定义的 selectImports
方法,将该方法的返回的字符串数组的类型添加到容器中
。
过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
configurations = this.getConfigurationClassFilter().filter(configurations);
自动装配的核心
本质上就是Spring容器的初始化
核心是自动配置类实现ImportSelect接口在selectImports方法中通过SPI机制,在自动配置包下,将META-INF下的spring.factory文件中把key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值获取到,并将value值转换为java类,注入Spring容器当中
自动装配的演示
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
@Autowired
private RedisTemplate<String,String>redisTemplate;
按照下面的顺序添加starter,然后添加配置,使用RedisTemplate就可以使用了? 那大家想没想过一个问题,为什么RedisTemplate可以被直接注入?它是什么时候加入到Ioc容器的呢? 这就是自动装配。自动装配可以使得classpath下依赖的包相关的bean,被自动装载到Spring Ioc容器中,怎么做到的呢?
深入分析EnableAutoConfiguration
EnableAutoConfiguration的主要作用其实就是帮助springboot应用把所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器中。
再回到EnableAutoConfiguration这个注解中,发现它的import是这样
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
但是从EnableAutoCOnfiguration上面的import注解来看,这里面并不是引入另外一个Configuration。而是一个ImportSelector。这个是什么东西呢?
AutoConfigurationImportSelector是什么?
Enable注解不仅仅可以像前面演示的案例一样很简单的实现多个Configuration的整合,还可以实现一些复杂的场景,比如可以根据上下文来激活不同类型的bean,@Import注解可以配置三种不同的class
- 第一种就是前面演示过的,基于普通bean或者带有@Configuration的bean进行诸如
- 实现ImportSelector接口进行动态注入
实现ImportBeanDefinitionRegistrar接口进行动态注入
CacheService
public class CacheService {
}
LoggerService
public class LoggerService {
}
EnableDefineService
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited --允许被继承
@Import({MyDefineImportSelector.class})
public @interface EnableDefineService {
String[] packages() default "";
}
MyDefineImportSelector
public class MyDefineImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//获得指定注解的详细信息。可以根据注解中配置的属性来返回不同的class,
//从而可以达到动态开启不同功能的目的
annotationMetadata.getAllAnnotationAttributes(EnableDefineService.class.getName(),true)
.forEach((k,v) -> {
log.info(annotationMetadata.getClassName());
log.info("k:{},v:{}",k,String.valueOf(v));
});
return new String[]{CacheService.class.getName()};
}
}
EnableDemoTest
@SpringBootApplication
@EnableDefineService(name = "mashibing",value = "mashibing")
public class EnableDemoTest {
public static void main(String[] args) {
ConfigurableApplicationContext ca=SpringApplication.run(EnableDemoTest.class,args);
System.out.println(ca.getBean(CacheService.class));
System.out.println(ca.getBean(LoggerService.class));
}
}
了解了selector的基本原理之后,后续再去分析AutoConfigurationImportSelector的原理就很简单了,它本质上也是对于bean的动态加载。
@EnableAutoConfiguration注解的实现原理
了解了ImportSelector和ImportBeanDefinitionRegistrar后,对于EnableAutoConfiguration的理解就容易一些了
它会通过import导入第三方提供的bean的配置类:AutoConfigurationImportSelector
@Import(AutoConfigurationImportSelector.class)
从名字来看,可以猜到它是基于ImportSelector来实现基于动态bean的加载功能。Springboot @Enable*注解的工作原理ImportSelector接口selectImports返回的数组(类的全类名)都会被纳入到spring容器中。
那么可以猜想到这里的实现原理也一定是一样的,定位到AutoConfigurationImportSelector这个类中的selectImports方法
selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 从配置文件(spring-autoconfigure-metadata.properties)中加载 AutoConfigurationMetadata
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 获取所有候选配置类EnableAutoConfiguration
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//获取元注解中的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//使用SpringFactoriesLoader 加载classpath路径下META-INF\spring.factories中,
//key= org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
//去重
configurations = removeDuplicates(configurations);
//应用exclusion属性
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
configurations = filter(configurations, autoConfigurationMetadata);
//广播事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
本质上来说,其实EnableAutoConfiguration会帮助springboot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里面借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持。以及用到了Spring提供的条件注解@Conditional,选择性的针对需要加载的bean进行条件过滤
SpringFactoriesLoader
为了给大家补一下基础,我在这里简单分析一下SpringFactoriesLoader这个工具类的使用。它其实和java中的SPI机制的原理是一样的,不过它比SPI更好的点在于不会一次性加载所有的类,而是根据key进行加载。
首先,SpringFactoriesLoader的作用是从classpath/META-INF/spring.factories文件中,根据key来加载对应的类到spring IoC容器中。接下来带大家实践一下
创建外部项目jar
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
创建bean以及config
public class mashibingCore {
public String study(){
System.out.println("good good study, day day up");
return "mashibingEdu.com";
}
}
@Configuration
public class mashibingConfig {
@Bean
public mashibingCore mashibingCore(){
return new mashibingCore();
}
}
创建另外一个工程(spring-boot)
把前面的工程打包成jar,当前项目依赖该jar包
<dependency>
<groupId>com.mashibingedu.practice</groupId>
<artifactId>mashibing-Core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
通过下面代码获取依赖包中的属性
运行结果会报错,原因是mashibingCore并没有被Spring的IoC容器所加载,也就是没有被EnableAutoConfiguration导入
@SpringBootApplication
public class SpringBootStudyApplication {
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext ac=SpringApplication.run(SpringBootStudyApplication.class, args);
mashibingCore Myc=ac.getBean(mashibingCore.class);
System.out.println(Myc.study());
}
}
解决方案
在mashibing-Core项目resources下新建文件夹META-INF,在文件夹下面新建spring.factories文件,文件中配置,key为自定配置类EnableAutoConfiguration的全路径,value是配置类的全路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.mashibingedu.practice.mashibingConfig
重新打包,重新运行SpringBootStudyApplication这个类。
可以发现,编写的那个类,就被加载进来了。
Spring Boot中的条件过滤
在分析AutoConfigurationImportSelector的源码时,会先扫描spring-autoconfiguration-metadata.properties文件,最后在扫描spring.factories对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢? 原因是很多的@Configuration其实是依托于其他的框架来加载的,如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以,通过这种条件过滤可以有效的减少@configuration类的数量从而降低SpringBoot的启动时间。
修改mashibing-Core
在META-INF/增加配置文件,spring-autoconfigure-metadata.properties。
com.mashibingedu.practice.mashibingConfig.ConditionalOnClass=com.mashibingedu.TestClass
格式:自动配置的类全名.条件=值
上面这段代码的意思就是,如果当前的classpath下存在TestClass,则会对mashibingConfig这个Configuration进行加载
演示过程(spring-boot)
-
沿用前面spring-boot工程的测试案例,直接运行main方法,发现原本能够被加载的mashibingCore,发现在ioc容器中找不到了。
public static void main(String[] args) throws IOException { ConfigurableApplicationContext ac=SpringApplication.run(SpringBootStudyApplication.class, args); mashibingCore Myc=ac.getBean(mashibingCore.class); System.out.println(Myc.study()); }
-
在当前工程中指定的包com.mashibingedu下创建一个TestClass以后,再运行上面这段代码,程序能够正常执行
手写Starter
通过手写Starter来加深对于自动装配的理解
1.创建一个Maven项目,quick-starter
定义相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
<!-- 可选 -->
<optional>true</optional>
</dependency>
2.定义Formate接口
定义的格式转换的接口,并且定义两个实现类
public interface FormatProcessor {
/**
* 定义一个格式化的方法
* @param obj
* @param <T>
* @return
*/
<T> String formate(T obj);
}
public class JsonFormatProcessor implements FormatProcessor {
@Override
public <T> String formate(T obj) {
return "JsonFormatProcessor:" + JSON.toJSONString(obj);
}
}
public class StringFormatProcessor implements FormatProcessor {
@Override
public <T> String formate(T obj) {
return "StringFormatProcessor:" + obj.toString();
}
}
3.定义相关的配置类
首先定义格式化加载的Java配置类
@Configuration
public class FormatAutoConfiguration {
@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
@Bean
@Primary // 优先加载
public FormatProcessor stringFormatProcessor(){
return new StringFormatProcessor();
}
@ConditionalOnClass(name="com.alibaba.fastjson.JSON")
@Bean
public FormatProcessor jsonFormatProcessor(){
return new JsonFormatProcessor();
}
}
定义一个模板工具类
public class HelloFormatTemplate {
private FormatProcessor formatProcessor;
public HelloFormatTemplate(FormatProcessor processor){
this.formatProcessor = processor;
}
public <T> String doFormat(T obj){
StringBuilder builder = new StringBuilder();
builder.append("Execute format : ").append("<br>");
builder.append("Object format result:" ).append(formatProcessor.formate(obj));
return builder.toString();
}
}
再就是整合到SpringBoot中去的Java配置类
@Configuration
@Import(FormatAutoConfiguration.class)
public class HelloAutoConfiguration {
@Bean
public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor){
return new HelloFormatTemplate(formatProcessor);
}
}
4.创建spring.factories文件
在resources下创建META-INF目录,再在其下创建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mashibingedu.autoconfiguration.HelloAutoConfiguration
install 打包,然后就可以在SpringBoot项目中依赖改项目来操作了。
5.测试
在SpringBoot中引入依赖
<dependency>
<groupId>org.example</groupId>
<artifactId>format-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在controller中使用
@RestController
public class UserController {
@Autowired
private HelloFormatTemplate helloFormatTemplate;
@GetMapping("/format")
public String format(){
User user = new User();
user.setName("BoBo");
user.setAge(18);
return helloFormatTemplate.doFormat(user);
}
}
6.自定义Starter关联配置信息
有些情况下可以需要用户在使用的时候动态的传递相关的配置信息,比如Redis的Ip,端口等等,这些信息显然是不能直接写到代码中的,这时就可以通过SpringBoot的配置类来实现。
首先引入依赖支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.6.RELEASE</version>
<optional>true</optional>
</dependency>
然后创建对应的属性类
@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX)
public class HelloProperties {
public static final String HELLO_FORMAT_PREFIX="yuyang.hello.format";
private String name;
private Integer age;
private Map<String,Object> info;
//set,get方法
}
然后再Java配置类中关联
@Configuration
@Import(FormatAutoConfiguration.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@Bean
public HelloFormatTemplate helloFormatTemplate(HelloProperties helloProperties,FormatProcessor formatProcessor){
return new HelloFormatTemplate(helloProperties,formatProcessor);
}
}
调整模板方法
public class HelloFormatTemplate {
private FormatProcessor formatProcessor;
private HelloProperties helloProperties;
public HelloFormatTemplate(HelloProperties helloProperties,FormatProcessor processor){
this.helloProperties = helloProperties;
this.formatProcessor = processor;
}
public <T> String doFormat(T obj){
StringBuilder builder = new StringBuilder();
builder.append("Execute format : ").append("<br>");
builder.append("HelloProperties:").append(formatProcessor.formate(helloProperties.getInfo())).append("<br>");
builder.append("Object format result:" ).append(formatProcessor.formate(obj));
return builder.toString();
}
}
增加提示
在这个工程的META-INF/下创建一个additional-spring-configuration-metadata.json,这个是设置属性的提示类型
{
"properties": [
{
"name": "yuyang.hello.format.name",
"type": "java.lang.String",
"description": "账号信息",
"defaultValue": "root"
},{
"name": "yuyang.hello.format.age",
"type": "java.lang.Integer",
"description": "年龄",
"defaultValue": 18
}
]
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 加载当前系统下 META-INF/spring.factories 文件中声明的配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除掉重复的
configurations = removeDuplicates(configurations);
// 移除掉显示排除的
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 过滤掉不需要载入的配置类
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
SpringBoot初始化核心流程源码
自动装配的时候为什么没有走selectImports方法?
分析SpringBoot自动装配源码的时候讲过在 @EnableAutoConfiguration
注解上通过 @Import
注解导入了一个 ImportSelector
接口的实现类 AutoConfigurationImportSelector
。按照之前对 @Import
注解的理解,应该会执行重写的 selectImports
方法,但调试的时候,执行的流程好像和期待的不一样哦,没有走 selectImports
方法。
通过Debug模式,端点定位能够发现进入到了getAutoConfigurationEntry方法中。
但是没有进入selectImports方法。
这是什么原因呢?他不是实现了ImportSelector接口吗?怎么和理解的不一样呢?这就需要再来细说下@Import注解了。
@Import
前面介绍过@Import注解可以根据添加的不同类型做出不同的操作
导入类型 | 注入方式 |
---|---|
实现了ImportSelector接口 | 不注入该类型的对象,调用selectImports方法,将返回的数据注入到容器中 |
实现了ImportBeanDefinitionRegistrar接口 | 不注入该类型的对象,调用registerBeanDefinitions方法,通过注册器注入 |
普通类型 | 直接注入该类型的对象 |
而在自动装配中导入的AutoConfigurationImportSelector这个类型有点特殊。具体看下类图结构
那这个DeferredImportSelector这个接口的作用是什么呢?字面含义是延迟导入的意思。具体怎么实现的后面再说,先来说下他的作用。
DeferredImportSelector接口
DeferredImportSelector接口本身也有ImportSelector接口的功能,如果仅仅是实现了DeferredImportSelector接口,重写了selectImports方法,那么selectImports方法还是会被执行的,来看代码。
public class MyDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("selectImports方法执行了---->");
return new String[0];
}
}
对应的配置启动类
@Configuration
@Import(MyDeferredImportSelector.class)
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
}
}
启动效果:
但是如果重写了DeferredImportSelector中的Group接口,并重写了getImportGroup,那么容器在启动的时候就不会执行selectImports方法了,而是执行getImportGroup方法。进而执行Group中重写的方法。
public class MyDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("selectImports方法执行了---->");
return new String[0];
}
@Override
public Class<? extends Group> getImportGroup() {
System.out.println("getImportGroup");
return MyDeferredImportSelectorGroup.class;
}
public static class MyDeferredImportSelectorGroup implements Group{
private final List<Entry> imports = new ArrayList<>();
@Override
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
System.out.println("MyDeferredImportSelectorGroup.Group");
}
@Override
public Iterable<Entry> selectImports() {
System.out.println("Group中的:selectImports方法");
return imports;
}
}
}
执行效果:
通过上面的效果解释了为什么在SpringBoot自动装配的时候没有走selectImports方法。那么DeferredImportSelector接口的作用是什么呢?为什么要这么设计呢?接下来继续分析
DeferredImportSelector的作用
通过前面的类图结构知道DeferredImportSelector是ImportSelector接口的一个扩展。
ImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理之前,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理(注意,这里只是对@Bean修饰的方法的处理,并不是立即调用@Bean修饰的方法,这个区别很重要!)
DeferredImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理完毕之后,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理.
上面的结论可以直接在源码中看到对应的答案。首先定位到ConfigurationClassParser中的parse方法。
上面代码有两个非常重要的分支,在下面逐一的介绍
1.parse方法
先看parse方法,也就是解析注解类的方法。进入
看到调用的是processConfigurationClass,翻译过来就比较好理解了,处理配置类
再进入到循环的方法中。
继续往下看
逻辑处理还是非常清楚的。然后需要回到上面的处理@Import注解的方法中。在这个方法中可以看到@Import注解的实现逻辑
也就是前面给大家回顾的@Import注解的作用
然后来看下导入的类型是ImportSelector接口的逻辑。
上面的代码重点解决了ImportSelector接口的不同类型的实现。
对应的实例存储了起来
2.process方法
好了上面的代码分析清楚了,然后再回到process方法中来看下DeferredImportSelectorHandler是如何处理的。
进入process方法
先看register方法
然后再看processGroupImports方法。
进去后需要进入getImports方法中。
然后进入到process方法中,可以看到自动装配的方法被执行了!
到这儿是不是帮助大家解决了自动装配为什么没有走 AutoConfigurationImportSelector
中的 selectImports
方法了!!!
同时也介绍清楚了ImportSelector与DeferredImportSelector的区别,就是selectImports方法执行时机有差别,这个差别期间,spring容器对此Configguration类做了些其他的逻辑:包括对@ImportResource、@Bean这些注解的处理
SpringBoot源码
对于想要研究SpringBoot源码的小伙伴来说,在本地编译源码环境,然后在研究源码的时候可以添加对应的注释是必须的,本文就给大家来介绍下如何来搭建我们的源码环境。
1.官方源码下载
首先大家要注意SpringBoot项目在2.3.0之前是使用Maven构建项目的,在2.3.0之后是使用Gradle构建项目的。后面分析的源码以SpringBoot2.2.5为案例,所以本文就介绍下SpringBoot2.2.5的编译过程。
官网地址:https://github.com/spring-projects/spring-boot
直接下载对于的压缩文件即可
下载后直接解压缩即可
2.本地源码编译
把解压缩的源码直接导入到IDEA中,修改pom文件中的版本号。
pom文件中提示 disable.checks
属性找不到,我们添加一个即可。
<properties>
<revision>2.2.5.snapshot</revision>
<main.basedir>${basedir}</main.basedir>
<!-- 添加属性 -->
<disable.checks>true</disable.checks>
</properties>
然后执行编译命令
mvn clean install -DskipTests
然后控制台出现如下错误
按照提示,执行下面的 命令 就好了:
在执行编译命令就可以了
mvn clean install -DskipTests
3.源码环境使用
既然源码已经编译好之后我们就可以在这个项目中来创建我们自己的SpringBoot项目了,我们在 spring-boot-project
项目下创建 module
,
然后在我们的module中添加对应的start依赖
然后添加我们的启动类
项目能够正常启动
同时点击run方法进去,我们可以添加注释了:
在其他项目使用我们编译的源码,这个可能是大家比较感兴趣的一个点了,我们也来介绍下,依赖我们还是可以使用官方的依赖即可,不过最好还是和我们编译的版本保持一致。
主要是关联上我们编译的源码。
修改代码
好了到此就可以开启SpringBoot的源码探索之旅了哦。
1.SpringBoot启动的入口
当我们启动一个SpringBoot项目的时候,入口程序就是main方法,而在main方法中就执行了一个run方法。
@SpringBootApplication
public class StartApp {
public static void main(String[] args) {
SpringApplication.run(StartApp.class);
}
}
2.run方法
然后我们进入run()方法中看。代码比较简单
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
// 调用重载的run方法,将传递的Class对象封装为了一个数组
return run(new Class<?>[] { primarySource }, args);
}
调用了重载的一个run()方法,将我们传递进来的类对象封装为了一个数组,仅此而已。我们再进入run()方法。
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// 创建了一个SpringApplication对象,并调用其run方法
// 1.先看下构造方法中的逻辑
// 2.然后再看run方法的逻辑
return new SpringApplication(primarySources).run(args);
}
public SpringApplication(Class<?>... primarySources) {
// 调用其他的构造方法
this(null, primarySources);
}
在该方法中创建了一个SpringApplication对象。同时调用了SpringApplication对象的run方法。这里的逻辑有分支,先看下SpringApplication的构造方法中的逻辑
3.SpringApplication构造器
SpringApplication的构造方法,看的核心代码为
public SpringApplication(Class<?>... primarySources) {
// 调用其他的构造方法
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 传递的resourceLoader为null无需关注
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 记录主方法的配置类名称
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 记录当前项目的类型,
//正常情况下 是SERVLET
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
// 并将加载的数据存储在了 initializers 成员变量中。
//实例化化并保存spring.factories文件中ApplicationContextInitializer的类型bean到initializers中
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器 并将加载的监听器实例对象存储在了listeners成员变量中
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 反推main方法所在的Class对象 并记录在了mainApplicationClass对象中
// 反推启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
//实例化化并保存spring.factories文件中ApplicationContextInitializer的类型bean到initializers中
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
/**
* 扩展点的加载
* @param type
* @param <T>
* @return
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
/**
* 初始化Initializer
* SpringFactoriesLoader.loadFactoryNames(type, classLoader)
* 根据对应的类型加载 spring.factories 文件中的配置信息
* @param type
* @param parameterTypes
* @param args
* @param <T>
* @return
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
// 获取当前上下文类加载器
ClassLoader classLoader = getClassLoader();
// 获取到的扩展类名存入set集合中防止重复
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建扩展点实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
//SpringFactoriesLoader.loadFactoryNames(type, classLoader)
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
//加载spring.factories中所有的信息到内存中
//(List)loadSpringFactories(classLoader)
//根据类型获取相关信息
//getOrDefault(factoryTypeName, Collections.emptyList())
}
// 反推启动类
this.mainApplicationClass = deduceMainApplicationClass();
/**
* StackTrace:
* 我们在学习函数调用时,都知道每个函数都拥有自己的栈空间。
* 一个函数被调用时,就创建一个新的栈空间。那么通过函数的嵌套调用最后就形成了一个函数调用堆栈
* @return
*/
private Class<?> deduceMainApplicationClass() {
try {
// 获取当前run方法执行的堆栈信息
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
// 遍历堆栈信息
for (StackTraceElement stackTraceElement : stackTrace) {
// 如果调用的是main方法说明就找到了
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
在本方法中完成了几个核心操作
- 推断当前项目的类型(正常为SERVLET)
- 加载配置在spring.factories文件中的ApplicationContextInitializer中的类型并实例化后存储在了initializers中。(加载spring.factories文件中所有的applicationcontextinitializer(初始化器)并保存)
- 和2的步骤差不多,完成监听器的初始化操作,并将实例化的监听器对象存储在了listeners成员变量中(加载spring.factories文件中所有的listeners(监听器)并保存)
- 通过StackTrace
反推main方法所在的Class对象
上面的核心操作具体的实现细节我们在后面的详细文章会给大家剖析
4.run方法
接下来再回到SpringApplication.run()方法中。
public ConfigurableApplicationContext run(String... args) {
// 创建一个任务执行观察器
StopWatch stopWatch = new StopWatch();
// 开始执行记录执行时间
stopWatch.start();
// 声明 ConfigurableApplicationContext 对象
ConfigurableApplicationContext context = null;
// 声明集合容器用来存储 SpringBootExceptionReporter 启动错误的回调接口
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 设置了一个名为java.awt.headless的系统属性
// 其实是想设置该应用程序,即使没有检测到显示器,也允许其启动.
//对于服务器来说,是不需要显示器的,所以要这样设置.
configureHeadlessProperty();
// 获取 SpringApplicationRunListener 加载的是 EventPublishingRunListener
// 获取启动时的监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 触发启动事件
listeners.starting();
try {
// 构造一个应用程序的参数持有类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 创建并配置环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 配置需要忽略的BeanInfo信息
configureIgnoreBeanInfo(environment);
// 输出的Banner信息
Banner printedBanner = printBanner(environment);
// 创建应用上下文对象
context = createApplicationContext();
// 加载配置的启动异常处理器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 刷新前操作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新应用上下文 完成Spring容器的初始化
refreshContext(context);
// 刷新后操作
afterRefresh(context, applicationArguments);
// 结束记录启动时间
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 事件广播 启动完成了
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 事件广播启动出错了
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 监听器运行中
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
// 返回上下文对象--> Spring容器对象
return context;
}
在这个方法中完成了SpringBoot项目启动的很多核心的操作,总结下上面的步骤
1. 创建了一个任务执行的观察器,统计启动的时间
2. 声明ConfigurableApplicationContext对象
3. 声明集合容器来存储SpringBootExceptionReporter即启动错误的回调接口
4. 设置java.awt.headless的系统属性
5. 获取我们之间初始化的监听器(EventPublishingRunListener),并触发starting事件
6. 创建ApplicationArguments这是一个应用程序的参数持有类
7. 创建ConfigurableEnvironment这时一个配置环境的对象
8. 配置需要忽略的BeanInfo信息
9. 配置Banner信息对象
10. 创建对象的上下文对象
11. 加载配置的启动异常的回调异常处理器
12. 刷新应用上下文,本质就是完成Spring容器的初始化操作
13. 启动结束记录启动耗时
14. 完成对应的事件广播
15. 返回应用上下文对象。
到此SpringBoot项目的启动初始化的代码的主要流程就介绍完成了。细节部分后面再说。
SpringApplication构造器
前面给大家介绍了SpringBoot启动的核心流程,本文开始给大家详细的来介绍SpringBoot启动中的具体实现的相关细节。 https://www.processon.com/view/link/61eab8f47d9c085d604e614d
首先来看下在SpringApplication的构造方法中是如何帮我们完成这4个核心操作的。
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 传递的resourceLoader为null
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 记录主方法的配置类名称
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 记录当前项目的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
// 并将加载的数据存储在了 initializers 成员变量中。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器 并将加载的监听器实例对象存储在了listeners成员变量中
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 反推main方法所在的Class对象 并记录在了mainApplicationClass对象中
this.mainApplicationClass = deduceMainApplicationClass();
}
1.webApplicationType
首先来看下webApplicationType是如何来推导出当前启动的项目的类型。通过代码可以看到是通过deduceFromClassPath()方法根据ClassPath来推导出来的。
this.webApplicationType = WebApplicationType.deduceFromClasspath();
跟踪进去看代码
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
在看整体的实现逻辑之前,我们先分别看两个内容,第一就是在上面的代码中使用到了相关的静态变量。
这些静态变量其实就是一些绑定的Java类的全类路径。第二个就是 ClassUtils.isPresent()
方法,该方法的逻辑也非常简单,就是通过反射的方式获取对应的类型的Class对象,如果存在返回true,否则返回false
所以到此推导的逻辑就非常清楚了
2.setInitializers
然后我们再来看下如何实现加载初始化器的。
// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
// 并将加载的数据存储在了 initializers 成员变量中。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
首先所有的初始化器都实现了 ApplicationContextInitializer
接口,也就是根据这个类型来加载相关的实现类。
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C var1);
}
然后加载的关键方法是 getSpringFactoriesInstances()
方法。该方法会加载 spring.factories
文件中的key为 org.springframework.context.ApplicationContextInitializer
的值。
spring-boot项目下
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
spring-boot-autoconfigure项目下
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
具体的加载方法为 getSpringFacotiesInstance()
方法,我们进入查看
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
// 获取当前上下文类加载器
ClassLoader classLoader = getClassLoader();
// 获取到的扩展类名存入set集合中防止重复
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建扩展点实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
先进入 SpringFactoriesLoader.loadFactoryNames(type, classLoader)
中具体查看加载文件的过程.
然后我们来看下 loadSpringFactories
方法
通过Debug的方式查看会更清楚哦
通过 loadSpringFactories
方法我们看到把 spring.factories
文件中的所有信息都加载到了内存中了,但是我们现在只需要加载 ApplicationContextInitializer
类型的数据。这时我们再通过 getOrDefault()
方法来查看。
进入方法中查看
然后会根据反射获取对应的实例对象。
好了到这其实我们就清楚了 getSpringFactoriesInstances
方法的作用就是帮我们获取定义在 META-INF/spring.factories
文件中的可以为 ApplicationContextInitializer
的值。并通过反射的方式获取实例对象。然后把实例的对象信息存储在了SpringApplication的 initializers
属性中。
3.setListeners
清楚了 setInitializers()
方法的作用后,再看 setListeners()
方法就非常简单了,都是调用了 getSpringFactoriesInstances
方法,只是传入的类型不同。也就是要获取的 META-INF/spring.factories
文件中定义的不同信息罢了。
即加载定义在 META-INF/spring.factories
文件中声明的所有的监听器,并将获取后的监听器存储在了 SpringApplication
的 listeners
属性中。
默认加载的监听器为:
4.mainApplicationClass
最后我们来看下 duduceMainApplicaitonClass()
方法是如何反推导出main方法所在的Class对象的。通过源码我们可以看到是通过 StackTrace
来实现的。
StackTrace: 我们在学习函数调用时,都知道每个函数都拥有自己的栈空间。 一个函数被调用时,就创建一个新的栈空间。那么通过函数的嵌套调用最后就形成了一个函数调用堆栈
StackTrace
其实就是记录了程序方法执行的链路。通过Debug方式可以更直观的来呈现。
那么相关的调用链路我们都可以获取到,剩下的就只需要获取每链路判断执行的方法名称是否是 main
就可以了。
SpringBoot中的监听机制详解
1.观察者模式
监听器的设计会使用到Java设计模式中的观察者模式,所以在搞清楚SpringBoot中的监听器的设计之前我们还是非常有必要把观察者模式先弄清楚。
观察者模式又称为发布/订阅(Publish/Subscribe)模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新.
在java.util包中包含有基本的Observer接口和Observable抽象类.功能上和Subject接口和Observer接口类似.不过在使用上,就方便多了,因为许多功能比如说注册,删除,通知观察者的那些功能已经内置好了.
1.1 定义具体被观察者
package com.dpb.observer2;
import java.util.Observable;
/**
* 目标对象
* 继承 Observable
* @author dengp
*
*/
public class ConcreteSubject extends Observable {
private int state;
public void set(int s){
state = s; //目标对象的状态发生了改变
setChanged(); //表示目标对象已经做了更改
notifyObservers(state); //通知所有的观察者
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
观察者只需要继承Observable父类。发送消息的方式执行如下两行代码即可
setChanged(); //表示目标对象已经做了更改
notifyObservers(state); //通知所有的观察者
Observable源码对应的是:
1.2 定义具体观察者
package com.dpb.observer2;
import java.util.Observable;
import java.util.Observer;
/**
* 观察者模式:观察者(消息订阅者)
* 实现Observer接口
* @author dengp
*
*/
public class ObserverA implements Observer {
private int myState;
@Override
public void update(Observable o, Object arg) {
myState = ((ConcreteSubject)o).getState();
}
public int getMyState() {
return myState;
}
public void setMyState(int myState) {
this.myState = myState;
}
}
观察者也就是订阅者只需要实现Observer接口并重写相关update方法即可,在目标实现中我们发现触发的时候执行的就是观察者的update方法。
1.3 测试
package com.dpb.observer2;
public class Client {
public static void main(String[] args) {
//创建目标对象Obserable
ConcreteSubject subject = new ConcreteSubject();
//创建观察者
ObserverA obs1 = new ObserverA();
ObserverA obs2 = new ObserverA();
ObserverA obs3 = new ObserverA();
//将上面三个观察者对象添加到目标对象subject的观察者容器中
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.addObserver(obs3);
//改变subject对象的状态
subject.set(3000);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
subject.set(600);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
//移除一个订阅者
subject.deleteObserver(obs2);
subject.set(100);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
}
}
这样就实现了官方提供观察者模式.
2.SpringBoot中监听器的设计
1.观察者模式
监听器的设计会使用到Java设计模式中的观察者模式,所以在搞清楚SpringBoot中的监听器的设计之前我们还是非常有必要把观察者模式先弄清楚。
观察者模式又称为发布/订阅(Publish/Subscribe)模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新.
在java.util包中包含有基本的Observer接口和Observable抽象类.功能上和Subject接口和Observer接口类似.不过在使用上,就方便多了,因为许多功能比如说注册,删除,通知观察者的那些功能已经内置好了.
1.1 定义具体被观察者
package com.dpb.observer2;
import java.util.Observable;
/**
* 目标对象
* 继承 Observable
* @author dengp
*
*/
public class ConcreteSubject extends Observable {
private int state;
public void set(int s){
state = s; //目标对象的状态发生了改变
setChanged(); //表示目标对象已经做了更改
notifyObservers(state); //通知所有的观察者
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
观察者只需要继承Observable父类。发送消息的方式执行如下两行代码即可
setChanged(); //表示目标对象已经做了更改
notifyObservers(state); //通知所有的观察者
Observable源码对应的是:
1.2 定义具体观察者
package com.dpb.observer2;
import java.util.Observable;
import java.util.Observer;
/**
* 观察者模式:观察者(消息订阅者)
* 实现Observer接口
* @author dengp
*
*/
public class ObserverA implements Observer {
private int myState;
@Override
public void update(Observable o, Object arg) {
myState = ((ConcreteSubject)o).getState();
}
public int getMyState() {
return myState;
}
public void setMyState(int myState) {
this.myState = myState;
}
}
观察者也就是订阅者只需要实现Observer接口并重写相关update方法即可,在目标实现中我们发现触发的时候执行的就是观察者的update方法。
1.3 测试
package com.dpb.observer2;
public class Client {
public static void main(String[] args) {
//创建目标对象Obserable
ConcreteSubject subject = new ConcreteSubject();
//创建观察者
ObserverA obs1 = new ObserverA();
ObserverA obs2 = new ObserverA();
ObserverA obs3 = new ObserverA();
//将上面三个观察者对象添加到目标对象subject的观察者容器中
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.addObserver(obs3);
//改变subject对象的状态
subject.set(3000);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
subject.set(600);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
//移除一个订阅者
subject.deleteObserver(obs2);
subject.set(100);
System.out.println("===============状态修改了!");
//观察者的状态发生了变化
System.out.println(obs1.getMyState());
System.out.println(obs2.getMyState());
System.out.println(obs3.getMyState());
}
}
这样就实现了官方提供观察者模式.
SpringBoot中默认的监听器
首先来回顾下SpringBoot中给我们提供的默认的监听器,这些都定义在spring.factories文件中。
监听器 | 监听事件 | 说明 |
---|---|---|
ClearCachesApplicationListener | ContextRefreshedEvent | 当触发ContextRefreshedEvent事件会清空应用的缓存 |
ParentContextCloserApplicationListener | ParentContextAvailableEvent | 触发ParentContextAvailableEvent事件会完成父容器关闭的监听器 |
CloudFoundryVcapEnvironmentPostProcessor | ApplicationPreparedEvent | 判断环境中是否存在VCAP_APPLICATION或者VCAP_SERVICES。如果有就添加Cloud Foundry的配置;没有就不执行任何操作。 |
FileEncodingApplicationListener | ApplicationEnvironmentPreparedEvent | 文件编码的监听器 |
AnsiOutputApplicationListener | ApplicationEnvironmentPreparedEvent | 根据 spring.output.ansi.enabled 参数配置 AnsiOutput |
ConfigFileApplicationListener | ApplicationEnvironmentPreparedEvent <br> ApplicationPreparedEvent | 完成相关属性文件的加载,application.properties application.yml 前面源码内容详细讲解过 |
DelegatingApplicationListener | ApplicationEnvironmentPreparedEvent | 监听到事件后转发给环境变量 context.listener.classes 指定的那些事件监听器 |
ClasspathLoggingApplicationListener | ApplicationEnvironmentPreparedEvent <br> ApplicationFailedEvent | 一个SmartApplicationListener,对环境就绪事件ApplicationEnvironmentPreparedEvent/应用失败事件ApplicationFailedEvent做出响应,往日志DEBUG级别输出TCCL(thread context class loader)的classpath。 |
LoggingApplicationListener | ApplicationStartingEvent <br> ApplicationEnvironmentPreparedEvent <br> ApplicationPreparedEvent <br> ContextClosedEvent <br> ApplicationFailedEvent | 配置 LoggingSystem 。使用 logging.config 环境变量指定的配置或者缺省配置 |
LiquibaseServiceLocatorApplicationListener | ApplicationStartingEvent | 使用一个可以和Spring Boot可执行jar包配合工作的版本替换liquibase ServiceLocator |
BackgroundPreinitializer | ApplicationStartingEvent <br> ApplicationReadyEvent <br> ApplicationFailedEvent | 尽早触发一些耗时的初始化任务,使用一个后台线程 |
SpringBoot中的事件类型
然后我们来看下对应的事件类型,SpringBoot中的所有的事件都是继承于 ApplicationEvent
这个抽象类,在SpringBoot启动的时候会发布如下的相关事件,而这些事件其实都实现了 SpringApplicationContext
接口。
事件 | 说明 |
---|---|
ApplicationStartingEvent | 容器启动的事件 |
ApplicationEnvironmentPreparedEvent | 应用处理环境变量相关的事件 |
ApplicationContextInitializedEvent | 容器初始化的事件 |
ApplicationPreparedEvent | 应用准备的事件 |
ApplicationFailedEvent | 应用启动出错的事件 |
ApplicationStartedEvent | 应用Started状态事件 |
ApplicationReadyEvent | 应用准备就绪的事件 |
也就是这些事件都是属于SpringBoot启动过程中涉及到的相关的事件
当然在启动过程中还会发布其他的相关事件,可以自行查阅相关源码哦
自定义事件
接下来通过几个自定义事件来加深下对事件监听机制的理解
监听所有事件
emsp; 我们先创建一个自定义监听器,来监听所有的事件。创建一个Java类,实现ApplicationListener接口在泛型中指定要监听的事件类型即可,如果要监听所有的事件,那么泛型就写ApplicationEvent。
之后为了在容器启动中能够发下我们的监听器并且添加到SimpleApplicationEventMulticaster中,我们需要在spring.factories中注册自定义的监听器
这样当我们启动服务的时候就可以看到相关事件发布的时候,我们的监听器被触发了。
监听特定事件
那如果是监听特定的事件呢,我们只需要在泛型出制定即可。
启动服务查看
自定义事件
那如果我们想要通过自定义的监听器来监听自定义的事件呢?首先创建自定义的事件类,非常简单,只需要继承ApplicationEvent即可
然后在自定义的监听器中监听自定义的事件。
同样的别忘了在spring.factories中注册哦
之后我们就可以在我们特定的业务场景中类发布对应的事件了
然后当我们提交请求后
可以看到对应的监听器触发了
这样一来不光搞清楚了SpringBoot中的监听机制,而且也可以扩展应用到我们业务开发中了。好了本文就给大家介绍到这里,希望对你有所帮助。
然后来看下SpringBoot启动这涉及到的监听器这块是如何实现的。
2.1 初始化操作
通过前面的介绍我们知道在SpringApplication的构造方法中会加载所有声明在spring.factories中的监听器。
通过Debug模式我们可以看到加载的监听器有哪些。
其实就是加载的spring.factories文件中的key为ApplicationListener的value
通过对这些内置监听器的源码查看我们发现这些监听器都实现了 ApplicationEvent
接口。也就是都会监听 ApplicationEvent
发布的相关的事件。ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。
2.2 run方法
然后我们来看下在SpringApplication.run()方法中是如何发布对应的事件的。
首先会通过getRunListeners方法来获取我们在spring.factories中定义的SpringApplicationRunListener类型的实例。也就是EventPublishingRunListener.
加载这个类型的时候会同步的完成实例化。
实例化操作就会执行EventPublishingRunListener.
在这个构造方法中会绑定我们前面加载的11个过滤器。
到这其实我们就已经清楚了EventPublishingRunListener和我们前面加载的11个监听器的关系了。然后在看事件发布的方法。
查看starting()方法。
再进入
进入到multicastEvent中方法中我们可以看到具体的触发逻辑
在这儿以ConfigFileApplicationListener为例。
触发会进入ConfigFileApplicationListener对象的onApplicationEvent方法中,
通过代码我们可以发现当前的事件是ApplicationStartingEvent事件,都不满足,所以ConfigFileApplicationListener在SpringBoot项目开始启动的时候就不会做任何的操作。而当我们在配置环境信息的时候,会发布对应的事件来触发
继续进入
继续进入
然后再触发ConfigFileApplicationListener监听器的时候就会触发如下方法了
其实到这儿,后面的事件发布与监听器的处理逻辑就差不多是一致了。到这儿对应SpringBoot中的监听器这块就分析的差不错了。像SpringBoot的属性文件中的信息什么时候加载的就是在这些内置的监听器中完成的。
官方内置的事件有:
好了本文就给大家介绍到这里,希望能对你有所帮助哦。
运行监听器,发布事件
SpringBoot中的属性文件加载原理
在创建SpringBoot项目的时候会在对应的application.properties或者application.yml文件中添加对应的属性信息,这些属性文件是什么时候被加载的?如果要实现自定义的属性文件怎么来实现呢?本文来给大家揭晓答案:
1.找到入口
结合前面介绍的SpringBoot中的监听事件机制,我们首先看下SpringApplication.run()方法,在该方法中会针对SpringBoot项目启动的不同的阶段来发布对应的事件。
处理属性文件加载解析的监听器是 ConfigFileApplicationListener
,这个监听器监听的事件有两个。
而我们进入SpringApplication.prepareEnvironment()方法中发布的事件其实就是ApplicationEnvironmentPreparedEvent事件。进入代码查看。
进行进入
继续进入会看到对应的发布事件:ApplicationEnvironmentPreparedEvent
结合上篇文件的内容,得知在initialMulticaster中是有ConfigFileApplicationListener这个监听器的。
那么在此处触发了配置环境的监听器,后续的逻辑就应该进入对应的
ConfigFileApplicationListener
主要流程分析
接下来我们看下ConfigFileApplicationListener中具体的如何来处理配置文件的加载解析的。
根据逻辑我们直接进入onApplicationEnvironmentPreparedEvent()方法中。
系统提供那4个不是重点,重点是 ConfigFileApplicationListener 中的这个方法处理.
直接进入ConfigFileApplicationListener.postProcessEnvironment()方法。
在进入addPropertySources()方法中会完成两个核心操作,1。创建Loader对象,2。调用Loader对象的load方法,
Loader构造器
现在我们来看下在Loader构造器中执行了什么操作。
通过源码我们可以发现在其中获取到了属性文件的加载器、从spring.factories文件中获取,对应的类型是 PropertySourceLoader
类型。
而且在loadFactories方法中会完成对象的实例化。
到这Loader的构造方法执行完成了,然后来看下load()方法的执行。先把代码贴上
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
// 创建默认的profile 链表
this.profiles = new LinkedList<>();
// 创建已经处理过的profile 类别
this.processedProfiles = new LinkedList<>();
// 默认设置为未激活
this.activatedProfiles = false;
// 创建loaded对象
this.loaded = new LinkedHashMap<>();
// 加载配置 profile 的信息,默认为 default
initializeProfiles();
// 遍历 Profiles,并加载解析
while (!this.profiles.isEmpty()) {
// 从双向链表中获取一个profile对象
Profile profile = this.profiles.poll();
// 非默认的就加入,进去看源码即可清楚
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 解析 profile
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 加载默认的属性文件 application.properties
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
然后我们进入具体的apply()方法中来查看。
中间的代码都有注释,主要是处理profile的内容。
首先是getSearchLocations()方法,在该方法中会查询默认的会存放对应的配置文件的位置,如果没有自定义的话,路径就是 file:./config/ file:./ classpath:/config/ classpath:/ 这4个
然后回到load方法中,遍历4个路径,然后加载对应的属性文件。
getSearchNames()获取的是属性文件的名称。如果自定义了就加载自定义的
否则加载默认的application文件。
再回到前面的方法
进入load方法,会通过前面的两个加载器来分别加载application.properties和application.yml的文件。
loader.getFileExtensions()获取对应的加载的文件的后缀。
进入loadForFileExtension()方法,对profile和普通配置分别加载
继续进入load方法
开始加载我们存在的application.properties文件。
properties加载
在找到了要加载的文件的名称和路径后,我们来看下资源加载器是如何来加载具体的文件信息的。
进入loadDocuments方法中,我们会发现会先从缓存中查找,如果缓存中没有则会通过对应的资源加载器来加载了。
此处是PropertiesPropertySourceLoader来加载的。
进入loadProperties方法
之后进入load()方法看到的就是具体的加载解析properties文件中的内容了。感兴趣的可以看下具体的逻辑,本文就给大家介绍到这里了。
SpringBoot源码分析之bootstrap.properties文件加载的原理
emsp; 对于SpringBoot中的属性文件相信大家在工作中用的是比较多的,对于application.properties和application.yml文件应该非常熟悉,但是对于bootstrap.properties文件和bootstrap.yml这个两个文件用的估计就比较少了,用过的应该清楚bootstrap.properties中定义的文件信息会先与application.properties中的信息加载。而且大家在使用的时候还经常碰到获取不到bootstrap.properties中定义的信息的困扰,本文就来给大家揭开这些谜团。
GitEE源码地址:https://gitee.com/dengpbs/spring-boot-2.2.5.snapshot.git
1.bootstrap的使用
首先在SpringBoot中默认是不支持bootstrap.properties属性文件的。需要映入SpringCloud的依赖才可以。
相关的版本环境
然后创建对应的bootstrap.properties文件,当然也可以创建bootstrap.yml文件
同步的我们也会创建application.properties文件,其中会覆盖一个属性
然后我们在controller中获取测试
访问测试:http://localhost:8080/query
通过访问看到bootstrap.properties中的信息获取到了,同时age也被application.properties中的属性覆盖掉了。加载顺序到底是什么?为什么会覆盖呢?接下来分析。
2.bootstrap加载原理分析
看本文之前最好看下前面讲解的SpringBoot中的监听机制。
2.1 BootstrapApplicationListener
在使用bootstrap.properties文件时我们需要映入相关的依赖
其实在这个依赖中会在对应的spring.factories文件中给我们提供新的监听器,也就是BootstrapApplicationListener监听器。
而BootstrapApplicationListener监听触发的事件是ApplicationEnvironmentPreparedEvent事件,这个事件其实和我们前面介绍监听application.properties的时候的监听器ConfigFileApplicationListener监听的是同一个事件。
如果你看了前面的文章,那么此处会觉得有点眉目了。也就是当启动的时候发布对应的事件,该监听器会触发相关的解析行为。
2.2 启动流程梳理
搞清楚了监听器的关系后,来看下启动的流程代码具体是怎么执行的。
直接进入
在SpringApplication的构造方法中我们要注意两点,1.监听器的加载 2.main方法的主类记录
然后回来进入run方法
Debug到第一个端点。
然后我们放过。
通过上面的动图可以看到又进入了一次这个run方法。先看处理的结果。
然后我们再放过,继续
分两次加载,有先右后哦。那么这里面的第一个加载的原理到底是什么呢?继续来分析。
2.3 bootstrap.properties的加载原理
接下来看看是如果出现的一个父context来优先加载我们的bootstrap.properteis文件的,还是从这个图开始
链路如上面一步步跟踪即可。
跳过非关键的,直接进入到BootstrapApplicationListener中来看。
然后进入到 bootstrapServiceContext方法中。
这儿我们看到有创建了一个SpringApplication对象。这个其实就是父Context对象了。
进入run方法你会发现,回到了前面
到这应该就清楚了执行的核心流程了
SpringBoot中的Tomcat容器加载
在创建应用上下文这一步创建Tomcat容器
Tomcat基础
想要搞清楚在SpringBoot启动中到的是如何集成的Tomcat容器,这个就需要先对Tomcat本身要有所了解,不然这个就没办法分析了。Tomcat版本是8.5.73
1.目录结构
先简单的回顾下一个Tomcat文件的目录结构
这个非常基础和简单就快速过掉。
2.启动流程
Tomcat的架构相关的内容在本文中就不再赘述,可以查阅Tomcat源码专题的内容,我们来看下当我们要启动一个Tomcat服务,我们其实是执行的bin目录下的脚本程序,startup.bat
和 startup.sh
.一个是windows的脚本,一个是Linux下的脚本,同样还可以看到两个停止的脚本 shutdown.bat
和 shutdown.sh
.
为了比较直观的来查看脚本的内容,我们通过VCCode来查看吧。
查看 startup.bat
可以看到在这个脚本中调用了 catalina.bat
这个脚本文件,继续进入,配置信息很多,找核心的脚本
对应的我们进入到doStart方法中
最后会执行的程序是
而这个MAINCLASS变量是前面定义的有的
其实前面看了这么一堆的脚本文件,都是在做一些环境的检测和运行时的参数,最终执行的是Bootstrap中的main方法。
3.Bootstrap类
3.1 架构图
在分析具体的源码流程之前还是需要对Tomcat的架构图要有所了解的
3.2 流程分析
接下来我们需要查看下Bootstrap中的main方法了,这时我们需要下载对应的源码文件了。可以官网自行下载,也可以在课件资料中找到。
本文不详解介绍,只为SpringBoot中内容做铺垫。
bootstrap.init(); // 初始化类加载器
bootstrap.load(); // 间接调用Catalina,创建对象树,然后调用生命周期的init方法初始化整个对象树
bootstrap.start(); // 间接调用Catalina的start方法,然后调用生命周期的start方法启动整个对象树
SpringBoot中详解
1.自动装配
首先我们来看下在spring.factories中注入了哪些和Web容器相关的配置类。
1.1 EmbeddedWebServerFactoryCustomizerAutoConfiguration
第一个是EmbeddedWebServerFactoryCustomizerAutoConfiguration。
查看代码,比较容易
在这个配置类里面就是根据我们的配置来内嵌对应的Web容器,比如Tomcat或者Jetty等。
1.2 ServletWebServerFactoryAutoConfiguration
然后来看下ServletWebServerFactoryAutoConfiguration这个配置类。
首先来看下在类的头部引入和一些核心的信息
重点我们需要看下EmbeddedTomcat这个内部类。
看到的核心其实是创建了一个TomcatServletWebServerFactory对象并注入到了Spring容器中。这块的内容非常重要,是我们后面串联的时候的一个切入点。
2.启动流程
有了上面的自动配置类的支持我们就可以看看在SpringBoot的run方法中是在哪个位置帮我们内嵌了Tomcat容器呢?首先我们从SpringBoot的run方法的刷新上下文的方法进入。
这部分其实就是Spring的核心代码了,我们进入到refresh()方法。
继续进入:
然后我们进入ServletWebServerApplicationContext对象的onRefresh方法中。
核心方法 createWebServer() 创建我们的Tomcat容器。
可以看到,从容器中获取的工厂对象其实就我们上面注入的对象,然后根据工厂对象获取到了一个TomcatWebServer实例,也就是Tomcat服务对象。关键点我们需要看下getWebServer方法的逻辑
然后继续进入到 getTomcatWebServer方法中。
进入构造方法查看
进入Tomcat初始化的方法initialize方法
进入start方法
到这儿后面的逻辑其实就是Tomcat自身启动的逻辑了。