前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >详解Spring Framework提供的扩展点:ApplicationContextInitializer应用上下文初始化器,以及它在SpringBoot中的应用【享学Spring】

详解Spring Framework提供的扩展点:ApplicationContextInitializer应用上下文初始化器,以及它在SpringBoot中的应用【享学Spring】

作者头像
YourBatman
发布2019-09-03 16:01:42
1.3K0
发布2019-09-03 16:01:42
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

前言

我事前百度了一下ApplicationContextInitializer的相关文章,无一例外全都是基于SpringBoot进行讲解的。

殊不知,这个类属于Spring Framework的而并非属于SpringBoot,so我认为开门见山就在SpringBoot里讲解它是欠缺妥当的。毕竟想要理解好SpringBoot,先了解Spring Framework才是第一要素

所以本文叙述的ApplicationContextInitializer完全是在Spring环境下的,和SpringBoot并无关联,希望它会成为一股清流,哈哈_

ApplicationContextInitializer

先啥都不说,先看看Spring的官方javadoc怎么解释此类:用于在刷新容器之前初始化Spring的回调接口。

任何一个SPI,它的执行时机特别特别的重要,所以这点必须重视

ApplicationContextInitializerSpring框架原有的概念, 这个类的主要目的就是在 ConfigurableApplicationContext类型(或者子类型)的ApplicationContext进行刷新refresh之前,允许我们对ConfigurableApplicationContext的实例做进一步的设置或者处理。

通常用于需要对应用程序进行某些初始化工作的web程序中。例如利用Environment上下文环境注册属性源、激活配置文件等等。

另外它支持Ordered@Order方式排序执行~

代码语言:javascript
复制
// @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中的应用

在我之前讲解ContextLoader之前,在之前博文:基于注解驱动的相关文章中,有重点讲述过此类。参考:

【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)

本文的关注点,自然只是ContextLoader里对ApplicationContextInitializer的处理:

代码语言:javascript
复制
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上下配置 + 本类自己配置的全部拿出来,在容器刷新之前执行而已。

在FrameworkServlet中的应用

这个是Spring MVC的核心API,被称为前端控制器。它会负责启动web子容器~

同样在这期间,它也会处理ApplicationContextInitializer

代码语言:javascript
复制
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的一模一样,没什么太值得说的。

如何自定义一个自己的ApplicationContextInitializer呢?

我们已经知道Spring内部并没有提供任何一个ApplicationContextInitializer的实现,

很显然这像是Spirng提供的一个SPI钩子接口,具体实现我们自己去定制接口。

上面说的都是ApplicationContextInitializer的它应用,它的执行。本处我们更应该关心的是:它是何时、怎么被注册进去的呢???

从上面源码分析也可以看出,在Spring环境下,我们自定义实现一个ApplicationContextInitializer让并且它生效的方式有两种:

  1. 手动调用他们的setXXX方法添加进去
  2. 通过ServletContext初始化参数放进去(其实如果是web.xml时代是配置即可)

其实此处有个小细节:此接口在Spring3.1开始提供的,所以很容易联想到它可以不依赖于web.xml配置方式,使用全注解驱动的方式也是可行的

借助WebApplicationInitializer方式自定义实现

我们的需求:需要在容器启动之前注册我们自己的ApplicationContextInitializer

我们知道Servlet3.0规范中提供了一个SPI来启动Spring容器,Spring对它进行了实现:

代码语言:javascript
复制
// @since 3.1
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	...
}

Servlet容器启动时,它会加载进来所有的**WebApplicationInitializer**接口实现类。

分别看看AbstractContextLoaderInitializerAbstractDispatcherServletInitializer它哥俩恰好就够提供了可扩展的方法:

代码语言:javascript
复制
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实现里,复写上述两个方法,即能达到自定义应用上下文初始化器的目的。

Demo Show

先写一个初始化器ApplicationContextInitializer实现类:

代码语言:javascript
复制
@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

代码语言:javascript
复制
/**
 * 自己实现 基于注解驱动的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};
    }
    ...
}

启动容器,能看到输入日志如下:(生效了)

代码语言:javascript
复制
/demo_war_war:Root WebApplicationContext
/demo_war_war:WebApplicationContext for namespace 'dispatcher-servlet'


SpringBoot中ApplicationContextInitializer的使用

思考良久,最终还是决定把该初始化器在SpringBoot中的应用也在此处一并说明了(毕竟这块的使用还是比较简单的,所以放一起吧

熟悉SpringBoot的小伙伴应该知道:它里面大量的使用到了Spring容器上下文启动的相关回调机制:比如SPI、事件/监听、启动器等等。

ApplicationContextInitializer是在springboot启动过程(refresh方法前)调用。提取部分源码参考如下:

代码语言:javascript
复制
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中自定义的步骤区别~

SpringBoot中自定义ApplicationContextInitializer的三种方式
spring.factories方式(官方就是这么用的,推荐)

不解释

application.properties添加配置方式

对于这种方式是通过DelegatingApplicationContextInitializer这个初始化类中的initialize方法获取到application.properties中的context.initializer.classes实现类的

所以只需要将实现了ApplicationContextInitializer的类添加到application.properties即可。示例如下:

代码语言:javascript
复制
context.initializer.classes=你的实现类全类名
API方式addInitializers()方法添加

形如:

代码语言:javascript
复制
	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内置的一些ApplicationContextInitializer

下面列出了一个使用缺省配置Springboot web应用默认所使用到的ApplicationContextInitializer实现们:

DelegatingApplicationContextInitializer

使用环境属性context.initializer.classes指定的初始化器(initializers)进行初始化工作,如果没有指定则什么都不做。

通过它使得我们可以把自定义实现类配置在application.properties里成为了可能

ContextIdApplicationContextInitializer

设置Spring应用上下文的ID,会参照环境属性。至于Id设置为啥值会参考环境属性:

spring.application.name

vcap.application.name

spring.config.name

spring.application.index

vcap.application.instance_index

如果这些属性都没有,ID使用application

ConfigurationWarningsApplicationContextInitializer

对于一般配置错误在日志中作出警告

ServerPortInfoApplicationContextInitializer

将内置servlet容器实际使用的监听端口写入到Environment环境属性中。这样属性local.server.port就可以直接通过@Value注入到测试中,或者通过环境属性Environment获取。

SharedMetadataReaderFactoryContextInitializer

创建一个SpringBoot和ConfigurationClassPostProcessor共用的CachingMetadataReaderFactory对象。实现类为:ConcurrentReferenceCachingMetadataReaderFactory

ConditionEvaluationReportLoggingListener

ConditionEvaluationReport写入日志。

以上都是SpringBoot内置的上文启动器,可见Spring留出的这个钩子,被SpringBoot发扬光大了。 实际上不仅于此,SpringBootSpring Framework的事件监听机制也都有大量的应用~

总结

ApplicationContextInitializer是Spring留出来允许我们在上下文刷新之前做自定义操作的钩子,若我们有需求想要深度整合Spring上下文,借助它不乏是一个非常好的实现

随便浏览一下SpringBoot的源码可知,它对Spring特征特性的使用,均是非常的流畅且深度整合的。所以说SpringBoot易学难精的最大拦路虎:其实是对Spring Framework系统性的把握~

Tips:spring-test包里有个注解org.springframework.test.context.ContextConfiguration它有个属性可以指定ApplicationContextInitializer辅助集成测试时候的自定义对上下文进行预处理~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年06月18日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • ApplicationContextInitializer
    • 在ContextLoader中的应用
      • 在FrameworkServlet中的应用
        • 如何自定义一个自己的ApplicationContextInitializer呢?
          • SpringBoot中ApplicationContextInitializer的使用
            • SpringBoot内置的一些ApplicationContextInitializer
            • 总结
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档