概述
本系列主要分析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); }
大家可以看一下上面的解釋,主要分為三段,總結下來就是:
- 這個接口的方法是在ConfigurableApplicationContext上下文中refresh()方法執行之前執行的一個回調
- 通常用在一些需要為應用上下文初始化的web應用中,比如,向容器中注冊屬性和激活配置
- 實現類可以使用@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
總結
三種實現方式已經介紹完了,如果大家只是想了解實現方式,不關心背后的源碼,那么到這里就可以結束了,下一篇文章我會從源碼中解釋這三種方式到底是如何加載的,如果大家感興趣可以繼續向下看。