Fork me on GitHub

springboot源码解析(一)-自定义系统初始化器

开篇之前先把祖师爷搬出来
  费玉清:问大家一个脑筋急转弯,说西方人在浴缸中洗澡,打一种小吃,小吃街里很常见的那种
      思考。。。
      思考。。。
      思考。。。
  揭晓谜底:涮羊肉
  反正谜底我已经揭晓了,至于大家能不能看到,我就不管了,哈哈

概述

  本系列主要分析springboot启动过程中干了什么事情,尽量以白话的形式写出来,因为本人也很小白,望包涵。

  系统初始化器是springboot在容器刷新之前执行的一个回调函数,其主要的作用就是向容器中注册属性,平时我们可能不会用到吧,但是在spring框架内部这个系统初始化器使用非常多,大家就当看看大佬是如何做的初始化,以及可以想一下他们为什么这样做,或许以后自己写程序也可以学习这种思想。

 

实现方式

  通过实现ApplicationContextInitializer接口来定义系统初始化器,这个接口是一个函数式接口,就是接口中只有一个方法,是java8的新特性。下面来看一下这个接口的定义。

/**
 * Callback interface for initializing a Spring {@link ConfigurableApplicationContext}
 * prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}.
 *
 * <p>Typically used within web applications that require some programmatic initialization
 * of the application context. For example, registering property sources or activating
 * profiles against the {@linkplain ConfigurableApplicationContext#getEnvironment()
 * context's environment}. See {@code ContextLoader} and {@code FrameworkServlet} support
 * for declaring a "contextInitializerClasses" context-param and init-param, respectively.
 *
 * <p>{@code ApplicationContextInitializer} processors are encouraged to detect
 * whether Spring's {@link org.springframework.core.Ordered Ordered} interface has been
 * implemented or if the @{@link org.springframework.core.annotation.Order Order}
 * annotation is present and to sort instances accordingly if so prior to invocation.
 *
 * @author Chris Beams
 * @since 3.1
 * @param <C> the application context type
 * @see org.springframework.web.context.ContextLoader#customizeContext
 * @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM
 * @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses
 * @see org.springframework.web.servlet.FrameworkServlet#applyInitializers
 */
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

    /**
     * Initialize the given application context.
     * @param applicationContext the application to configure
     */
    void initialize(C applicationContext);

}

大家可以看一下上面的解释,主要分为三段,总结下来就是:

  1. 这个接口的方法是在ConfigurableApplicationContext上下文中refresh()方法执行之前执行的一个回调
  2. 通常用在一些需要为应用上下文初始化的web应用中,比如,向容器中注册属性和激活配置
  3. 实现类可以使用@Order注解修饰,在调用之前可以对实例进行排序(注:@Order可以决定bean的执行顺序,值越小优先级越高)

上面对应用初始化器做了一个简单的介绍,并且看了应用初始化器的接口定义,下面就使用具体的例子来实战,自定义一些初始化,验证一下,这个先提前说一下,应用初始化有3种注入方式,具体看下面的例子。

第一种方式:使用META-INF/spring.factories

1.自定义系统初始化器

@Order(1)
public class FirstApplicationContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> {

    @Override
    public void initialize(GenericApplicationContext context) {
        ConfigurableEnvironment environment = context.getEnvironment();
        Map<String,String> map = new HashMap<>();
        map.put("first","hello first");
        environment.getSystemProperties().putAll(map);
        System.out.println("firstApplicationContextInitializer is start");
    }
}

解释一下上面代码:这个代码很简单,就是向系统环境中添加一个新的属性,属性的key是first,value是hello first,然后当这个系统初始化器被执行的时候会打印firstApplicationContextInitializer is start

 

2.在resource目录下新建META-INF目录,之后在META-INF目录下新建文件spring.factories,在文件中添加如下代码

org.springframework.context.ApplicationContextInitializer=com.example.demo.initialize.FirstApplicationContextInitializer

解释:等号前面是系统初始化器接口的路径,这个不要改,等号后面是自定义的具体实现类,路径要写自己程序的这个类所在的路径

 

3.写一个controller,然后后去系统的环境,看看能不能后去到在系统初始化器中添加的first属性

@RestController
public class InitializeController {

    @Autowired
    ApplicationContext applicationContext;

    @GetMapping("/first")
    public String test(){
        String a = applicationContext.getEnvironment().getProperty("first");
        return a;
    }

    @GetMapping("/second")
    public String test1(){
        String a = applicationContext.getEnvironment().getProperty("second");
        return a;
    }

    @GetMapping("/third")
    public String test2(){
        String a = applicationContext.getEnvironment().getProperty("third");
        return a;
    }

}

ok,做完以上3部,就可以启动springboot程序了,下面是我的启动程序时的打印结果。

 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)
firstApplicationContextInitializer is start
2020-05-29 10:20:52.989  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on ganxinledeMacBook-Pro.local with PID 1576 (/Users/ganxinle/workspace/demo/target/classes started by ganxinle in /Users/ganxinle/workspace/demo)
2020-05-29 10:20:52.991  INFO 1576 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-05-29 10:20:53.686  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-05-29 10:20:53.696  INFO 1576 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-05-29 10:20:53.696  INFO 1576 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 721 ms
2020-05-29 10:20:53.860  INFO 1576 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-29 10:20:53.979  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-29 10:20:53.982  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.258 seconds (JVM running for 1.577)
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-05-29 10:21:12.354  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms

大家可以看到我标红的地方,就是打印的结果,下面在浏览器中调用http://localhost:8080/first

可以看到已经获取到first属性的值。

总结:第一种实现方式已经介绍完毕,大家看过之后肯定有疑问,为什么要在resource目录下搞一个META-INF,还新建一个spring.factories,为什么在spring.factories中要那样配置,憋着机,等我介绍完3中方式之后,会把源码扒拉出来,让大家看一下具体的原因,没兴趣看源码的,到时候可以不用看那部分,哈哈😄,放个屁放松一下。

 第二种方式:通过直接添加的方式

1.自定义系统初始化器

@Order(1)
public class SecondApplicationContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> {

    @Override
    public void initialize(GenericApplicationContext context) {
        ConfigurableEnvironment environment = context.getEnvironment();
        Map<String,String> map = new HashMap<>();
        map.put("second","hello second");
        environment.getSystemProperties().putAll(map);
        System.out.println("secondApplicationContextInitializer is start");
    }
}

解释:这个和上面那个系统初始化器没什么区别,就是把first改成了second

 

2.修改main方法中springboot执行方式

在Springboot的启动的时候原始写法如下:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

改成下面的写法就可以实现在启动的时候直接把系统初始化器注入进去

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(DemoApplication.class);
        application.addInitializers(new SecondApplicationContextInitializer());
        application.run(args);
    }
}

启动程序,打印结果如下

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)
firstApplicationContextInitializer is start
secondApplicationContextInitializer is start
2020-05-29 10:20:52.989  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on ganxinledeMacBook-Pro.local with PID 1576 (/Users/ganxinle/workspace/demo/target/classes started by ganxinle in /Users/ganxinle/workspace/demo)
2020-05-29 10:20:52.991  INFO 1576 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-05-29 10:20:53.686  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-05-29 10:20:53.696  INFO 1576 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-05-29 10:20:53.696  INFO 1576 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 721 ms
2020-05-29 10:20:53.860  INFO 1576 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-29 10:20:53.979  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-29 10:20:53.982  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.258 seconds (JVM running for 1.577)
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-05-29 10:21:12.354  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms

大家可以看到红色的部分,就是初始化器已经开始执行了,在浏览器中访问http://localhost:8080/second

总结:看了上面的实现,大家有没有发现一个问题在两个系统初始化器中@Order(1),优先级设置的都是1,为什么是first先打印,而second后打印呢?同样的道理,这个后面会在源码中找到原因

 

第三种方式:通过application.properties中配置

1.自定义系统初始化器

@Order(1)
public class ThirdApplicationContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> {

    @Override
    public void initialize(GenericApplicationContext context) {
        ConfigurableEnvironment environment = context.getEnvironment();
        Map<String,String> map = new HashMap<>();
        map.put("third","hello third");
        environment.getSystemProperties().putAll(map);
        System.out.println("thirdApplicationContextInitializer is start");
    }
}

2.在配置文件application.perproties中配置如下配置项

context.initializer.classes=com.example.demo.initialize.ThirdApplicationContextInitializer

同样的,这里的路径要根据自己的路径配置。

启动程序,可以看到如下打印结果

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

thirdApplicationContextInitializer is start
firstApplicationContextInitializer is start
secondApplicationContextInitializer is start
2020-05-29 10:20:52.989  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on ganxinledeMacBook-Pro.local with PID 1576 (/Users/ganxinle/workspace/demo/target/classes started by ganxinle in /Users/ganxinle/workspace/demo)
2020-05-29 10:20:52.991  INFO 1576 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-05-29 10:20:53.686  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-05-29 10:20:53.696  INFO 1576 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-05-29 10:20:53.696  INFO 1576 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-05-29 10:20:53.749  INFO 1576 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 721 ms
2020-05-29 10:20:53.860  INFO 1576 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-29 10:20:53.979  INFO 1576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-29 10:20:53.982  INFO 1576 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.258 seconds (JVM running for 1.577)
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-29 10:21:12.351  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-05-29 10:21:12.354  INFO 1576 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms

可以看到,三个系统初始化器都已经执行了,但是大家有没有发现是第三个系统初始化器先执行的,这个原因同样一会再分析。访问http://localhost:8080/third

总结

  三种实现方式已经介绍完了,如果大家只是想了解实现方式,不关心背后的源码,那么到这里就可以结束了,下一篇文章我会从源码中解释这三种方式到底是如何加载的,如果大家感兴趣可以继续向下看。

posted @ 2020-05-29 12:14  猿起缘灭  阅读(928)  评论(0编辑  收藏  举报