看完SpringBoot源码后,整个人都精神了!

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

这是Spring系列最后一篇核心文章。

当读完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,主要由以下几点:

  1. 自动配置原理
  2. 内嵌Tomcat原理
  3. @EnableWebMvc做了什么
  4. WebMvcConfigurationSupport和WebMvcConfigurer区别
  5. jar启动原理
  6. 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.classargs);
   }
}

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(thisthis.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<MyServlet> servletRegistrationBean() {
      return new ServletRegistrationBean<MyServlet>(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>保存所有WebMvcConfigurer实现类,重写的方法都会遍历这个集合,调用其相同的方法。

那么这个集合是哪来的?

可以看DelegatingWebMvcConfiguration中下面这个方法,标有@Autowired,说明Spring会自动调用这个方法,并且把容器中所有实现WebMvcConfigurer接口的类实例化后传递到这里。

@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> 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(nullnew 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();
}

- END -


原文始发于微信公众号(十四个字节):看完SpringBoot源码后,整个人都精神了!