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

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

前言


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

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


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


ApplicationContextInitializer


先啥都不说,先看看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中的应用


在我之前讲解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上下配置 + 本类自己配置的全部拿出来,在容器刷新之前执行而已。


在FrameworkServlet中的应用

这个是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的一模一样,没什么太值得说的。


如何自定义一个自己的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对它进行了实现:

// @since 3.1
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
  ...
}

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


image.png


分别看看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实现里,复写上述两个方法,即能达到自定义应用上下文初始化器的目的。


Demo Show


先写一个初始化器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'
相关文章
|
5天前
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
2月前
|
缓存 Java 数据库连接
Spring Boot奇迹时刻:@PostConstruct注解如何成为应用初始化的关键先生?
【8月更文挑战第29天】作为一名Java开发工程师,我一直对Spring Boot的便捷性和灵活性着迷。本文将深入探讨@PostConstruct注解在Spring Boot中的应用场景,展示其在资源加载、数据初始化及第三方库初始化等方面的作用。
58 0
|
3天前
|
监控 Java 应用服务中间件
Spring和Spring Boot的区别
Spring和Spring Boot的主要区别,包括项目配置、开发模式、项目依赖、内嵌服务器和监控管理等方面,强调Spring Boot基于Spring框架,通过约定优于配置、自动配置和快速启动器等特性,简化了Spring应用的开发和部署过程。
29 19
|
2天前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
26 2
|
2天前
|
前端开发 安全 Java
【Spring】Spring Boot项目创建和目录介绍
【Spring】Spring Boot项目创建和目录介绍
14 2
|
4天前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
22 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
29天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
23天前
|
Kubernetes Cloud Native Java
当 Quarkus 遇上 Spring Boot,谁才是现代云原生应用的终极之选?究竟哪款能助你的应用傲视群雄?
Quarkus 和 Spring Boot 均为构建现代云原生应用的热门框架,旨在简化开发流程并提升性能。Spring Boot 依托庞大的 Spring 生态系统,提供开箱即用的体验,适合快速搭建应用。Quarkus 由红帽发起,专为 GraalVM 和 HotSpot 设计,强调性能优化和资源消耗最小化,是云原生环境的理想选择。
24 3
|
2月前
|
Java 微服务 Spring
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
文章介绍了如何利用Spring Cloud Alibaba快速构建大型电商系统的分布式微服务,包括服务限流降级等主要功能的实现,并通过注解和配置简化了Spring Cloud应用的接入和搭建过程。
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
|
2月前
|
安全 Java 数据安全/隐私保护
基于SpringBoot+Spring Security+Jpa的校园图书管理系统
本文介绍了一个基于SpringBoot、Spring Security和JPA开发的校园图书管理系统,包括系统的核心控制器`LoginController`的代码实现,该控制器处理用户登录、注销、密码更新、角色管理等功能,并提供了系统初始化测试数据的方法。
40 0
基于SpringBoot+Spring Security+Jpa的校园图书管理系统