我事前百度了一下ApplicationContextInitializer
的相关文章,无一例外全都是基于SpringBoot
进行讲解的。
殊不知,这个类属于Spring Framework
的而并非属于SpringBoot
,so我认为开门见山就在SpringBoot
里讲解它是欠缺妥当的。毕竟想要理解好SpringBoot
,先了解Spring Framework
才是第一要素
所以本文叙述的ApplicationContextInitializer
完全是在Spring环境下的,和SpringBoot
并无关联,希望它会成为一股清流,哈哈_
先啥都不说,先看看Spring的官方javadoc怎么解释此类:用于在刷新容器之前初始化Spring的回调接口。
任何一个SPI,它的执行时机特别特别的重要,所以这点必须重视
ApplicationContextInitializer
是Spring
框架原有的概念, 这个类的主要目的就是在 ConfigurableApplicationContext
类型(或者子类型)的ApplicationContext
进行刷新refresh
之前,允许我们对ConfigurableApplicationContext
的实例做进一步的设置或者处理。
通常用于需要对应用程序进行某些初始化工作的web程序中
。例如利用Environment
上下文环境注册属性源、激活配置文件等等。
另外它支持Ordered
和@Order
方式排序执行~
// @since 3.1
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
// Initialize the given application context.
void initialize(C applicationContext);
}
此接口,Spring Framework
自己没有提供任何的实现类。但小伙伴们可能都知道SpringBoot
对它有较多的扩展实现。
本文因为纯在Spring Framework环境下,所以主要讲解它在org.springframework.web.context.ContextLoader
以及org.springframework.web.servlet.FrameworkServlet
中的应用。最后我会示例怎么样通过自定义的方式实现容器刷新前的自定义行为。
在我之前讲解ContextLoader
之前,在之前博文:基于注解驱动的相关文章中,有重点讲述过此类。参考:
【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)
本文的关注点,自然只是ContextLoader
里对ApplicationContextInitializer
的处理:
public class ContextLoader {
...
// 它是个list,也就是说可以向此容器注册N上下文初始化器
private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers = new ArrayList<>();
// 为啥不用addAll? 因为此处泛型需要强转~
public void setContextInitializers(@Nullable ApplicationContextInitializer<?>... initializers) {
if (initializers != null) {
for (ApplicationContextInitializer<?> initializer : initializers) {
this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer);
}
}
}
// ======================它的执行时机如下;======================
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
...
configureAndRefreshWebApplicationContext(cwac, servletContext);
...
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
...
customizeContext(sc, wac);
wac.refresh();
...
}
// 由此可见所有的ApplicationContextInitializer的执行,都发生在wac.refresh();之前
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
//这里是从ServletContext 读取配置
//因为我们可以这么配置的globalInitializerClasses:ApplicationContextInitializer实现类的全类名(可以逗号分隔配置多个)
// 或者key是它:contextInitializerClasses
// 拿到配置的这些全类名后,下面都要反射创建对象的~~~
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses = determineContextInitializerClasses(sc);
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format(
"Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
// 创建好实例对象后,添加进全局的list里面(默认使用空构造函数)
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
// 采用order排序后,再分别执行~~~~
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
...
}
可以看到ContextLoader
中对ApplicationContextInitializer
的使用还是非常简单。它所做无非就是把ServletContext
上下配置 + 本类自己配置的全部拿出来,在容器刷新之前执行而已。
这个是Spring MVC
的核心API,被称为前端控制器。它会负责启动web子容器~
同样在这期间,它也会处理ApplicationContextInitializer
:
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers = new ArrayList<>();
// 添加的代码和上面一模一样~
public void setContextInitializers(@Nullable ApplicationContextInitializer<?>... initializers) {
if (initializers != null) {
for (ApplicationContextInitializer<?> initializer : initializers) {
this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer);
}
}
}
// 执行时机
@Override
protected final void initServletBean() throws ServletException {
...
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
...
}
protected WebApplicationContext initWebApplicationContext() {
...
configureAndRefreshWebApplicationContext(cwac);
...
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
...
postProcessWebApplicationContext(wac);
applyInitializers(wac);
wac.refresh();
}
protected void applyInitializers(ConfigurableApplicationContext wac) {
// 此处可以看到,只会拿globalInitializerClasses这个key配置的了~~~~ 只有全局的此处才会继续有用
String globalClassNames = getServletContext().getInitParameter(ContextLoader.GLOBAL_INITIALIZER_CLASSES_PARAM);
if (globalClassNames != null) {
for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
this.contextInitializers.add(loadInitializer(className, wac));
}
}
// 本Servlet里不仅仅可以直接set进来,也可以contextInitializerClasses直接配置全类名
if (this.contextInitializerClasses != null) {
for (String className : StringUtils.tokenizeToStringArray(this.contextInitializerClasses, INIT_PARAM_DELIMITERS)) {
this.contextInitializers.add(loadInitializer(className, wac));
}
}
// 最终排序、执行~~~
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
}
整体的执行流程,几乎和ContextLoader
的一模一样,没什么太值得说的。
我们已经知道Spring
内部并没有提供任何一个ApplicationContextInitializer
的实现,
很显然这像是Spirng
提供的一个SPI钩子接口,具体实现我们自己去定制接口。
上面说的都是ApplicationContextInitializer
的它应用,它的执行。本处我们更应该关心的是:它是何时、怎么被注册进去的呢???
从上面源码分析也可以看出,在Spring环境下,我们自定义实现一个ApplicationContextInitializer
让并且它生效的方式有两种:
ServletContext
初始化参数放进去(其实如果是web.xml时代是配置即可)其实此处有个小细节:此接口在Spring3.1
开始提供的,所以很容易联想到它可以不依赖于web.xml
配置方式,使用全注解驱动的方式也是可行的
我们的需求:需要在容器启动之前注册我们自己的ApplicationContextInitializer
。
我们知道Servlet3.0
规范中提供了一个SPI
来启动Spring容器,Spring对它进行了实现:
// @since 3.1
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
...
}
Servlet容器启动时,它会加载进来所有的**WebApplicationInitializer
**接口实现类。
分别看看AbstractContextLoaderInitializer
和AbstractDispatcherServletInitializer
它哥俩恰好就够提供了可扩展的方法:
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
// 它最终会被本类的此处调用:listener.setContextInitializers(getRootApplicationContextInitializers());
// 我们知道ContextLoaderListener所有事情都是委托给了ContextLoader类去完成的~
@Nullable
protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
return null;
}
}
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
// 它最终会本类的此处调用dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
@Nullable
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return null;
}
}
由此可见,我们只需要在自己写的WebApplicationInitializer
实现里,复写上述两个方法,即能达到自定义应用上下文初始化器的目的。
先写一个初始化器ApplicationContextInitializer
实现类:
@Order(10)
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 该方法此处不能用 因为还没初始化会报错:call 'refresh' before accessing beans via the ApplicationContext
//int beanDefinitionCount = applicationContext.getBeanDefinitionCount();
System.out.println(applicationContext.getApplicationName() + ":" + applicationContext.getDisplayName());
}
}
再复写WebApplicationInitializer
实现类的相关方法:把我们自定义的初始化器return
/**
* 自己实现 基于注解驱动的ServletInitializer来初始化DispatcherServlet
*/
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return new ApplicationContextInitializer[]{new MyApplicationContextInitializer()};
}
@Override
protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
return new ApplicationContextInitializer[]{new MyApplicationContextInitializer()};
}
/**
* 根容器的配置类;(Spring的配置文件) 父容器;
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class, JdbcConfig.class, AsyncConfig.class, ScheduldConfig.class};
}
/**
* web容器的配置类(SpringMVC配置文件) 子容器;
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebMvcConfig.class};
}
...
}
启动容器,能看到输入日志如下:(生效了)
/demo_war_war:Root WebApplicationContext
/demo_war_war:WebApplicationContext for namespace 'dispatcher-servlet'
ApplicationContextInitializer
的使用思考良久,最终还是决定把该初始化器在SpringBoot
中的应用也在此处一并说明了(毕竟这块的使用还是比较简单的,所以放一起吧)
熟悉SpringBoot
的小伙伴应该知道:它里面大量的使用到了Spring容器上下文启动的相关回调机制:比如SPI、事件/监听、启动器等等。
ApplicationContextInitializer是在springboot启动过程(refresh方法前)调用。提取部分源码参考如下:
public class SpringApplication {
// 对象初始化的时候 会从spring.factories里拿出来
private void initialize(Object[] sources) {
...
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
...
}
public ConfigurableApplicationContext run(String... args) {
...
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
...
}
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
...
applyInitializers(context);
...
}
// 执行处 把所有的遍历执行(已经排序)getInitializers里会根据order进行排序
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
}
了解了SpringBoot
加载、执行ApplicationContextInitializer
的过程,就可以很容易总结出在SpringBoot
中自定义使用ApplicationContextInitializer
的三种方式:
请注意在SpringBoot中自定义和在Spring Framework中自定义的步骤区别~
不解释
对于这种方式是通过DelegatingApplicationContextInitializer
这个初始化类中的initialize方法获取到application.properties
中的context.initializer.classes
实现类的
所以只需要将实现了ApplicationContextInitializer
的类添加到application.properties
即可。示例如下:
context.initializer.classes=你的实现类全类名
addInitializers()
方法添加形如:
public static void main(String[] args) {
//type01
SpringApplication springApplication = new SpringApplication(Application.class);
// 添加进去
springApplication.addInitializers(new Demo01ApplicationContextInitializer());
springApplication.run(args);
//SpringApplication.run(InitializerDemoApplication.class,args);
}
虽然三种方式都可以,但个人比较推荐的方式为通过
spring.factories
方式配置
下面列出了一个使用缺省配置的Springboot web
应用默认所使用到的ApplicationContextInitializer
实现们:
使用环境属性context.initializer.classes
指定的初始化器(initializers)进行初始化工作,如果没有指定则什么都不做。
通过它使得我们可以把自定义实现类配置在
application.properties
里成为了可能
设置Spring应用上下文的ID,会参照环境属性。至于Id设置为啥值会参考环境属性:
spring.application.name
vcap.application.name
spring.config.name
spring.application.index
vcap.application.instance_index
如果这些属性都没有,ID使用application
。
对于一般配置错误在日志中作出警告
将内置servlet容器实际使用的监听端口写入到Environment环境属性中。这样属性local.server.port
就可以直接通过@Value
注入到测试中,或者通过环境属性Environment
获取。
创建一个SpringBoot和ConfigurationClassPostProcessor
共用的CachingMetadataReaderFactory
对象。实现类为:ConcurrentReferenceCachingMetadataReaderFactory
将ConditionEvaluationReport
写入日志。
以上都是
SpringBoot
内置的上文启动器,可见Spring留出的这个钩子,被SpringBoot
发扬光大了。 实际上不仅于此,SpringBoot
对Spring Framework
的事件监听机制也都有大量的应用~
ApplicationContextInitializer
是Spring留出来允许我们在上下文刷新之前做自定义操作的钩子,若我们有需求想要深度整合Spring上下文,借助它不乏是一个非常好的实现。
随便浏览一下SpringBoot
的源码可知,它对Spring
特征特性的使用,均是非常的流畅且深度整合的。所以说SpringBoot
易学难精的最大拦路虎:其实是对Spring Framework
系统性的把握~
Tips:spring-test
包里有个注解org.springframework.test.context.ContextConfiguration
它有个属性可以指定ApplicationContextInitializer
辅助集成测试时候的自定义对上下文进行预处理~