SpringBoot解析
前言
我对spring的理解就像一张零碎的残图,这次记录就想把这残图给整理拼凑起来,哪怕后面忘了,也方便在查看,毕竟按照自己风格记录的还是要记得快一点
目前自己的理解也只是皮毛,更多的是看大佬的解析慢慢深入,但现在也可以比较顺的把基础加载原理理出来(参考文章1:[传送门],参考文章2:[传送门]),参考文章3:[传送门]),参考文章4:refreshContext[传送门],参考文章5:[传送门],参考文章6:[传送门]
spring是一个通过java开发出来的框架,本质还是通过java面向对象这些类,对象构造出来的一个框架。核心的本质:就是解决对象的创建,管理和注入,销毁的所有过程。SpringBoot严格来说应该不算是一个新框架,而是对Spring进行封装的脚手架,AOP和IOC核心功能是Spring就有的,而Springboot开始,需要更简洁方便的导入第三方依赖,加入了自动装配,免去了繁琐的配置
轮廓
这里先引用三张图片,比较直观的能看到整个启动流程
图一:
图二:
图三:
很多文章其实介绍的很清楚,但原本自己是没有概念或者缺少经验,总是会忽略或者看的很懵逼,这里就从简入简,先分成三个部分来说
1. 启动类上面的注解@SpringBootApplication
这是个复合注解,里面包含了多个注解,它主要的功能就是自动装配,自动扫描本地依赖及满足要求的三方依赖注入ioc(此处会在第三部分中的refreshContext方法来调用)
2. 启动类中的main方法:源码中new SpringApplication部分
这里就属于正式启动整个项目的初始化部分了,主要作用就是确定程序类型,初始化一些容器,监听器等等,就是做好后续的准备工作(这里会调用自动装配去加载spring.factories相应的依赖)
3. 启动类中的main方法:源码中run部分
这部分是最复杂的了,一入断点深似海,整个启动的部分基本都在这里,一系列的程序加载都在这里完成,不用全去了解,重点去关注里面的这个方法 this.refreshContext(context);,它就是去完成当前项目中所有bean(config,service,controller等等)的装配,包括三方依赖start的加载(注解中的功能也就是现在进行解析,注解只是一个标记而已,肯定是有方法去识别它并通过反射拿到想要的内容去处理)
描边
这就根据上面的三个部分去做略微深入
第一部分:注解
点进来就看到这三个注解
@SpringBootConfiguration:就是@Configuration,表示当前是配置类,不多说
@EnableAutoConfiguration
可以发现它是一个复合注解,其实@AutoConfigurationPackage里面也是一个@Import
@import是什么?@import表明导⼊⼀个或者多个组件类,这个组件类通常是配置类,也可以是⼀个实现selectImport接口的子类
而@AutoConfigurationPackage这里是导入的**AutoConfigurationPackages.Registrar**的组件,作用是会把启动类所在目录及其子包作为bean定义注册,供@ComponentScan自动扫描这些包下的组件,供后续使用.参考文章:[传送门]
然后在回到@Import(AutoConfigurationImportSelector.class),点**AutoConfigurationImportSelector**,
发现两个重要的东西
selectImports()方法根据参数AnnotationMetadata值返回所有候选配置类全限定名,
getExclusionFilter()方法从候选配置类中筛选出满足条件的候选配置类。
里面的源码又能追溯下去,来不及了,快上车.主要功能就是通过SpringFactoriesLoader类来加载每个三方包"META-INF/spring.factories"文件,对满足条件的配置类进行ioc注入,这里放张图,我觉得很不错,可以更好的理解自动装配
@ComponentScan
扫描当前包(启动类路径)以及⼦包下的加了注解的bean类; 如果类上标注@Component、@Service、@Controller、@Repository等注解。并且使用的是该注解的默认值(启动类的包),那么只扫码被注解类当前包以及⼦包下的bean类,所以知道,为什么启动类需要放在最外层了吗
第二部分:new SpringApplication()
从这里开始就正式启动部分了,源码点进来发现,启动分为了两部
1. 创建SpringApplication实例
2. 调用run方法
点进SpringApplication,发现都是在判断应用程序类型,初始化一些上下文及监听器之类的
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//把sources设置到SpringApplication的sources属性中
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//推断应用的类型:创建的是 REACTIVE应用、SERVLET应用、NONE 三种中的某一种
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//使用 SpringFactoriesLoader查找并加载 classpath下 META-INF/spring.factories文件中所有可用的 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//使用 SpringFactoriesLoader查找并加载 classpath下 META-INF/spring.factories文件中的所有可用的 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//推断并设置 main方法的定义类
this.mainApplicationClass = deduceMainApplicationClass();
}
第三部分:run方法
public ConfigurableApplicationContext run(String... args) {
//应用启动计时器器StopWatch 对象,来统计启动时间
StopWatch stopWatch = new StopWatch();
//计时开始(后面有个对应的计时结束)
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
//该接口对SpringApplicaiton的run方法不同阶段进行监听
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
//将执行run方法时传入的参数封装成一个对象
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//配置环境变量信息(包括maven和系统的环境变量)
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
// banner配置和打印
Banner printedBanner = this.printBanner(environment);
//创建ApplicationContext上下文容器
context = this.createApplicationContext();
//异常报告器是用来捕捉全局异常使用的,当springboot应用程序在发生异常时,异常报告器会将其捕捉并做相应处理,在spring.factories 文件里配置了默认的异常报告器
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//准备上下文环境
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//容器初始化(重要,自动装配就在这里完成)
this.refreshContext(context);
//启动后的一些处理,目前里面是空的
this.afterRefresh(context, applicationArguments);
//应用启动计时结束, 到这应用启动就结束了
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
//告诉程序启动成功
listeners.started(context);
//扩展功能,可以在启动完成后执行自定义的run方法;有2中方式可以实现:①实现 ApplicationRunner ②接口实现 CommandLineRunner 接口
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
相关知识点
1, 钩子函数
本质就是接口,贯穿于SpringBean的⽣命周期中,用于扩展程序,进⾏业务逻辑的增强.就好比第三方依赖,比如mybatis-plus之类的,都是依赖钩子函数,只要遵守规则,即可对原有的bean进行增强改造
2, 应该有很多人都看到过这个问题,spring是单例的吗? 当初自己也很蒙蔽,这什么鬼,什么单例多例,指什么意思?
其实它指的是,springioc加载bean的作用域是单例还是多例,这里就得知道一个注解@Scope,spring默认是单例模式,当ioc加载完后,所有的bean实例个都是独一份,不会重复创建,是被共用的,可被多次调用
( 题外话:容器实例是启动时立即加载(可用@Lazy改变),多例模式默认是懒加载 )
singleton 单实例的(单例)(默认) ----全局有且仅有一个实例
prototype 多实例的(多例) ---- 每次获取Bean的时候会有一个新的实例
reqeust 同一次请求 ----request:每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效
session 同一个会话级别 ---- session:每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效
3, 为什么springioc不管理pojo?
嗯..问得好,下次不要在问了.这是因为ioc管理bean在空间中都只有一份,而pojo里面有属性,基本都涉及到了修改操作,我刚传进来一个属性A,下一次马上就传进来一个B,这种情况下覆盖性的操作,是不可能管理的,同时pojo数量也不小,如果加进来对内存的损耗也很大的
4. Controller中成员变量线程安全吗?如果想用该怎么办
不安全,该变量并不具有隔离性,属性会被覆盖.可以使用线程副本Threadlocal,保证每个变量是当前线程使用,具有安全性,同时这个是弱引⽤,不⽤担⼼在堆中创建多个age导致资源浪费,⼀旦资源不⾜,会引发GC,又由于弱引⽤优先剔除的原则,所以会很快被清理的。
ps:我们new出来的对象,⼀般称之为强引⽤
总结
1. 这真的是个庞大的家伙,要啃透还是很费劲的,上述都是参考众大佬文章后再加上自己跟着断点走,慢慢理解的一点小东西,顺便做了个小总结,部分引用了他们的文章内容,仅供参考,有不对的还请麻烦指正
2. 另外启动的部分这里也只是对每个部分做了个简单的描述,还是需要自己打断点结合这大佬的文章去细看的,包括里面 继承/实现 了谁,哪个接口有什么用是需要去了解的,还回涉及到的设计模式也可以结合着去了解