看完SpringBoot源码后,整个人都精神了!
共 15319字,需浏览 31分钟
·
2022-04-23 22:29
作者:i听风逝夜
链接:https://juejin.cn/post/7067714190424670239
前言
当读完SpringBoot源码后,被Spring的设计者们折服,Spring系列中没有几行代码是我们看不懂的,而是难在理解设计思路,阅读Spring、SpringMVC、SpringBoot需要花时间,由于没开学,在家前前后后花了17天天才懂了个大概,这期间也加上写文章时间,并对每一个知识点进行代码验证,也花了不少,如果除去这些的话,我觉得10天左右就能熟悉他们。
光Spring核心就有3000多个java文件,Web模块有1368个,而SpringBoot比较少,只有1880多个,看到这些数字让我们有个轻重,知道该从哪读起。
还有推荐一本老书,2006年的,叫《Spring框架高级编程》,Spring原班人马写的,别看他老,他很少讲代码,而是说Spring设计思想。
正文
我们在前几章已经说完了Spring、SpringMVC,这章说最后一章SpringBoot,很多人不知道他们区别,在这里简单说下,首先他们三个是依次诞生的,后一个依赖前一个,Spring最为一个底层支撑尤为重要,他的核心就是我们常说的IOC容器和AOP,容器里面存放的就是实例化后的对象,AOP依靠两个技术来实现,CGLIB和JDK的Proxy,如果单独使用Spring开发程序,我们可以做桌面程序,也可以做命令行程序,但如果要做后端接口,那这就需要我们自己实现Servlet了,并且必须非常熟悉Spring。
但后来有了SpringMVC,他的出现导致再也不用写Servlet了,有了一种更方便的写法,也就是在一个类上标记@Controller注解,再配合@GetMapping,就能实现一个接口,SpringMVC还提供了参数转换器,能让我们更加方便的从请求中获取参数,SpringMVC的核心是DispatcherServlet,可以在上一章了解。
后来SpringBoot诞生了,SpringBoot并没有什么新技术,他更多的是组装能力,他是在SpringMVC的基础上,把Servlet容器又隐藏掉了,因为在搭建SpringMVC的时候,还需要配置如Tomcat,这也是麻烦的一步,索性SpringBoot把配置他的过程也封装掉,那么这就导致一个新手可以在几分钟内,实现一个后端API。
但也不仅仅是隐藏掉Servlet容器那么简单。
那么下面我们深入分析一下SpringBoot,主要由以下几点:
自动配置原理 内嵌Tomcat原理 @EnableWebMvc做了什么 WebMvcConfigurationSupport和WebMvcConfigurer区别 jar启动原理 war启动原理
自动配置原理
我们常说的自动配置也并不是什么厉害的东西,他是在解决Spring扫描不到的问题,我们在前几章说过,对象能进入容器中的前提,是标有@Component注解并且能让Spring扫描到,或者手动注册。
那么想这样一个问题,你的Spring应用开发完了,打包成jar后上线部署,但有一天需要增加功能,并且这个功能需要在独立的jar中,而你的主工程要依赖这个jar,但是麻烦的是需要要重新修改主工程中scan的路径,或者加入@ComponentScan,这样主工程中才能管理这个jar中的对象。
而有一天这个主工程还要依赖其他小组开发的jar,并且包名也不一样,那么还要在主工程中加入@ComponentScan。
这是不是有点麻烦?
那么有人就提出来了,我们能不能制定一个约定,外部的jar遵守这个约定,主项目中按照这个约定进行加载?
这里的约定就是一个文件,文件中放入想被Spring管理的class类名,而主项目中扫描到这个jar中有这个文件后,就把文件内容中的class类名拿出来注册到容器。
那么这个文件就是常说的spring.factories,开启按照约定加载就是通过@EnableAutoConfiguration。
@EnableAutoConfiguration可能都听过,他标注在@SpringBootApplication上,
@EnableAutoConfiguration注解上又有一个@Import注解,@Import用来向容器导入一些bean,注解本身没有什么用,想了解他是什么原理,做了什么,需要找到调用他的地方。
对于一个下面这种方法启动的SpringBoot程序来说,我们要了解上面注解的工作原理,第一个关键信息在ConfigurationClassPostProcessor下。
@SpringBootApplication
public class SampleWebUiApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(SampleWebUiApplication.class, args);
}
}
复制代码
ConfigurationClassPostProcessor类会在Spring进行refresh()的时候执行,详细信息可以看以往文章的Spring第二章。
ConfigurationClassPostProcessor会对SampleWebUiApplication进行解析,我们把标注在SampleWebUiApplication类上的的注解放在一起看,首先这个类有@ComponentScan注解,那么在ConfigurationClassPostProcessor执行的时候就会提取出@ComponentScan上的信息,进行bean扫描,Spring规定,如果@ComponentScan上的basePackages为空,那么默认的basePackages会成为他所在的包,这也就是为什么普遍要把"启动类"放在最外层,这样他的所有子包才能得到扫描。
下一步是判断有没有@Import注解,当然有了,就是下面这个。
@Import(AutoConfigurationImportSelector.class)
复制代码
如果存在@Import,就实例化他的值,这里的值就是AutoConfigurationImportSelector,并且有三种情况,其中一种情况是这个值继承ImportSelector,那么Spring会认定这个类用于向容器导入新的bean,则会调用他的selectImports。
而这个类就是我们自动配置的关键类。
他会获取所有jar中位于路径META-INF/spring.factories下所有org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值,如果你随便打开一个spring.factories文件,就会看到或多或少有下面这样的配置。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration
复制代码
他是通过SpringFactoriesLoader加载的,这个类是Spring中的类,但是很少在Spring中看到他的使用。
这就是上面我们所说的约定,这样任何一个jar中只有遵循这个约定,就可以向Spring中注册对象,下面简单看下这段源码,如果要详细研究,重点对象还是SpringFactoriesLoader。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
/**
* 获取所有jar中位于路径META-INF/spring.factories下所有`org.springframework.boot.autoconfigure.EnableAutoConfiguration`的值
*/
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
/**
* 转换成数组返回
*/
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
复制代码
这就是自动配置,非常简单,但是给我们带来的好处却有很多。
内嵌Tomcat原理
这个首先得Tomcat本身支持,如果Tomcat不支持内嵌,SpringBoot估计也没办法,或者可能会另找出路。
Tomcat本身有一个Tomcat类,没错就叫Tomcat,全路径是org.apache.catalina.startup.Tomcat,我们想启动一个Tomcat,直接new Tomcat(),之后调用start()就可以了。
并且他提供了添加Servlet、配置连接器这些基本操作。
下面看一个例子。
public class Main {
public static void main(String[] args) {
try {
Tomcat tomcat =new Tomcat();
tomcat.getConnector();
tomcat.getHost();
Context context = tomcat.addContext("/", null);
tomcat.addServlet("/","index",new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello");
}
});
context.addServletMappingDecoded("/","index");
tomcat.init();
tomcat.start();
}catch (Exception e){}
}
}
复制代码
启动后访问http://localhost:8080/
就可以看到响应hello。
那么接下来就是分析SpringBoot在什么地方创建的Tomcat,以及在什么地方添加的DispatcherServlet。
首先需要了解Spring本身提供的扩展函数,因为SpringBoot都是在Spring的基础上做的。
这需要关注AbstractApplicationContext的refresh()方法,Spring的启动流程离不开这个方法,其中调用了一个模板方法onRefresh(),在他的注释上也已经说了,用来留下做扩展,调用这个方法的时候所有bean已经被收集完成了,但是还没进行实例化。
我们开发Web应用的时候,他的实现类都是AnnotationConfigServletWebServerApplicationContext,而创建Tomcat就是在他重写的onRefresh()下,如下。
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
复制代码
private void createWebServer() {
WebServer webServer = this.webServer;
/**
* ServletContext是Tomcat启动后通过ServletContainerInitializer回调过来的
*/
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
/**
* 服务创建工厂,
*/
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
/**
* Tomcat启动后会回调到 {@link ServletWebServerApplicationContext#selfInitialize(ServletContext)}
*/
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
复制代码
这里有两个关键,一个是Web服务创建工厂,即ServletWebServerFactory,因为有很多Web服务器类型,除了Tomcat,还有Jetty、Undertow,每个工厂创建的服务器都会被封装在WebServer对象中并返回,WebServer提供了基本的启动、停止服务的方法,而实例化对应的ServletWebServerFactory,并不是直接new,他要走Spring的创建流程,因为这里设计到一个扩展,有一个WebServerFactoryCustomizerBeanPostProcessor的后置处理器负责调用所有WebServerFactoryCustomizer的customize()方法,用来自定义服务配置,在这里可以修改监听的端口等信息。
那么一定会有一个地方向Spring添加WebServerFactoryCustomizerBeanPostProcessor,其实就是在spring.factories下,负责导入的类是ServletWebServerFactoryAutoConfiguration。
二是ServletContainerInitializer,这是Servlet规范中定义的,由各个Servlet容器去调用,用来做一些初始化工作,通常把他的实现类放入jar中的META-INF/services/javax.servlet.ServletContainerInitializer下,Tomcat会自己扫描,如果有的话就会调用,还有就是通过Context的addServletContainerInitializer,这个Context表示Tomcat中的一个Web应用实例。
回调的时候主要会传入ServletContext。
而SpringBoot使用第二种方法,在Tomcat创建成功后,生成TomcatStarter对象,把这个对象给Tomcat送过去,将来在Tomcat启动成功后,会回调他的onStartup。
但是难在TomcatStarter中的onStartup会调用所有他保存的ServletContextInitializer对象,注意了,这是两个不同的对象。
ServletContainerInitializer是Servlet规范中的。
ServletContextInitializer是SpringBoot中的。
复制代码
而他所保存的实例中有一个是方法引用传递的。
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
复制代码
所以最终会回调到这里,注意这里是被Tomcat所调用的。
private void selfInitialize(ServletContext servletContext) throws ServletException {
/**
* 通过Lambda在Tomcat OnStart阶段时候会回调到这里
*/
/**
* 设置ServletContext下文
*/
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
/**
* 从容器中获取目前所有ServletContextInitializer,并调用
*
* 最关键的是DispatcherServletRegistrationBean
*/
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
复制代码
到这里就有意思了,TomcatStarter中默认会有3个实例,干不同的事,注意这3个实例都不在Spring容器中。这3个其中一个就是上面那个selfInitialize方法,此方法具体做的事就是拿到Spring容器中所有ServletContextInitializer的实例,并依次调用其onStartup()方法,而其中一个实例是DispatcherServletRegistrationBean,看名字就知道这个对象肯定有点东西。
我们进入DispatcherServletRegistrationBean的onStartup看做了什么,这里就不跟踪源码了,他做得事就是向ServletContext上下文中添加DispatcherServlet。
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}
复制代码
但是添加了Servlet,还没给他作映射,那么接下configure()就是干这个的,我们几乎没有用过ServletRegistration.Dynamic,这也是Servlet规范中的,不是SpringBoot,他的其中作用就是添加映射,让指定的url能到达此Servlet,获取他就是通过上面addServlet,而DispatcherServlet的url路径就是/
。
刚才我们说了,他会拿到Spring容器中所有ServletContextInitializer的实例,并依次调用,那么我们就可以自己实现一个ServletContextInitializer,标记上@Component,也可以达到添加Servlet的效果。
但是SpringBoot提供了ServletRegistrationBean,这个是专门向Web容器中添加Servlet的,那么我们借助他可以这样做。
@Configuration
public class Test {
static class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello");
}
}
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new MyServlet(), "/test");
}
}
复制代码
到这里就结束了,DispatcherServlet都已经添加到容器中了,那还有两个小问题,DispatcherServlet在什么地方生成、DispatcherServletRegistrationBean在什么地方生成,根据SpringBoot的命名规则,肯定是DispatcherServletAutoConfiguration,所有AutoConfiguration结尾的类几乎都会在spring.factories下。
@EnableWebMvc做了什么
SpringBoot中没有使用过@EnableWebMvc,至少我发现,因为@EnableWebMvc的作用就是向容器导如DelegatingWebMvcConfiguration,而这个类已经由WebMvcAutoConfiguration中导入了。
具体DelegatingWebMvcConfiguration做了什么,我们下面说。
WebMvcConfigurationSupport和WebMvcConfigurer区别
我们先说WebMvcConfigurationSupport,DelegatingWebMvcConfiguration是继承他的。
他的作用是向容器导入HandlerMapping、ViewResolver等DispatcherServlet中需要的组件,不导入行不行,也行,因为在DispatcherServlet中首先会从容器中查找,如果没有,则会生成默认的,默认创建后也不会加入到Spring容器中。
我们通常继承WebMvcConfigurationSupport添加一些资源或者参数转换器,会不会觉得很神奇,SpringBoot自己就调用了如addResourceHandlers方法,其实这些方法都是在创建DispatcherServlet中需要的组件时候调用的,比如下面,这个返回的是处理静态资源的对象。
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper());
//调用此方法添加自定义资源路径
addResourceHandlers(registry);
......
return handlerMapping;
}
复制代码
SpringBoot导入的是EnableWebMvcConfiguration,他们三个是继承关系。
EnableWebMvcConfiguration》DelegatingWebMvcConfiguration》WebMvcConfigurationSupport
复制代码
EnableWebMvcConfiguration中同样会导入一些bean,EnableWebMvcConfiguration位于源码WebMvcAutoConfiguration中,属于内部类,但是在WebMvcAutoConfiguration上SpringBoot通过下面注解表示,如果已经有了WebMvcConfigurationSupport实例,那么我就不会再向容器导入了,也就是WebMvcConfigurationSupport在SpringBoot中只会保留一个。
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
复制代码
但并不是说你有好几个类继承了WebMvcConfigurationSupport,最终只有一个会被Spring管理,而是Spring在解析类的时候,会循环解析他的父类了,并且这个父类这能被解析一次,他里面标有@Bean的方法也只会调用一次。
这也就是如果A、B两个类都继承WebMvcConfigurationSupport,你所有重写的方法,在A、B中只有一个会被调用。
那么如果你想通过好几个类配置怎么办,那就需要WebMvcConfigurer了,如果你看DelegatingWebMvcConfiguration源码,他重写的方法都很简单,内部会有一个List
保存所有WebMvcConfigurer实现类,重写的方法都会遍历这个集合,调用其相同的方法。
那么这个集合是哪来的?
可以看DelegatingWebMvcConfiguration中下面这个方法,标有@Autowired,说明Spring会自动调用这个方法,并且把容器中所有实现WebMvcConfigurer接口的类实例化后传递到这里。
@Autowired(required = false)
public void setConfigurers(List configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
复制代码
那么我们如果想在多个类中配置,实现WebMvcConfigurer接口既可,而在SpringBoot中也会通过这个接口默认增加一些配置,比如下面这些资源通过url就可以直接访问。
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/"
复制代码
但是这就有个问题,如果我继承了WebMvcConfigurationSupport,那么DelegatingWebMvcConfiguration就没办法实例化,所有实现了WebMvcConfigurer的接口就得不到执行,是不是这样呢?
确实是的。
但还有个问题,只要继承了WebMvcConfigurationSupport,那么上面SpringBoot为你配置的默认资源访问路径,也会失效,就是因为SpringBoot的自动配置,发现如果有了WebMvcConfigurationSupport,那么自己就不会添加了。
那不是说添加默认资源访问路径是在WebMvcConfigurer接口中的吗?
没错,但这个接口实现类在WebMvcAutoConfiguration下,也属于内部类。
解决办法就是不要继承WebMvcConfigurationSupport,我们应该实现WebMvcConfigurer接口,这是最好的办法。
jar启动原理
SpringBoot有两种线上部署方法,jar和war,先说jar原理。
要运行一个jar,首先需要在META-INF/MANIFEST.MF配置Main-Class,随便打开一个SpringBoot项目,都会发现Main-Class都是org.springframework.boot.loader.JarLauncher。
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.springdemo.SpringDemoApplicationKt
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.5.6
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码
那么我们就顺藤摸瓜进入这个类的main方法。
但看来看去,简单的不得了,就是接着提取上面属性中的Start-Class,通过反射调用他的main方法,这个类就是我们开发的时候的"主类"了。
那为什么不直接把Main-Class设置成我们的主类?
其实也可以,但是这就麻烦多了,主要原因是jar文件中,不能包含其他第三方jar,很多文章说的jar禁止嵌套jar也是说的这个意思,但是我专门去官网找jar格式说明,没有找到关于禁止这一类的词,更多的是关于第三方lib的都需要配置在Class-Path下,这个路径是相对主jar路径搜索的。
一般普通java项目打包成jar后,如果第三方jar也要被打入,那么打包插件通常都会把第三方jar解压后连同主项目编译后的目录一同放入jar中,不会把整个jar放在主jar文件中,这样会报找不到类,
如果SpringBoot使用这种方式把Spring所有依赖和其他依赖都解压后放入主jar中,那么会导致混乱不堪。
最终SpringBoot的这种方式纯属是被迫。
他的实现思想是既然JVM不能加载jar中的jar,那么SpringBoot自己实现一个ClassLoader来加载,然后赋值给当前线程的ClassLoader,最终通过反射加载我们的主类时,指定一个ClassLoader。
public void run() throws Exception {
Class> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
复制代码
这也就是打包运行后,输出某个类的ClassLoader是LaunchedURLClassLoader的原因。
而这个LaunchedURLClassLoader就负责加载BOOT-INF下的class和jar。
war启动原理
war就是由Tomcat来加载了,启动原理也比较简单,依靠ServletContainerInitializer来完成,上面说过,这是由Tomcat来回调,但还有一个注解没有说@HandlesTypes,这个注解用来标注在ServletContainerInitializer的实现类上,参数是个Class类型,表示告诉Tomcat,对我进行回调的时候,把所有继承或者和这个Class相等的Class给我传过来。
而ServletContainerInitializer的实现类,需要放到META-INF/services/javax.servlet.ServletContainerInitializer
下,但是SpringBoot并没有实现这个约定,实现了约定的是spring-web模块,他的定义如下。
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}
复制代码
可以看到,他要求Tomcat回传所有WebApplicationInitializer的实现类给他,那么细心的你可能发现,如果要部署在Tomcat中,必须要继承SpringBootServletInitializer,重写其中configure方法并配置我们的主类,而这个类正好又实现了WebApplicationInitializer接口。
而SpringServletContainerInitializer中做的只是实例化传递过来的所有WebApplicationInitializer实现类,调用他们的onStartup()。
那么这就到了上面说的SpringBootServletInitializer.onStartup下,最终会根据我们所配置的主类,同样调用SpringApplication.run启动。
protected WebApplicationContext run(SpringApplication application) {
return (WebApplicationContext) application.run();
}