详解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'
相关文章
|
19天前
|
监控 Java 数据库连接
详解Spring Batch:在Spring Boot中实现高效批处理
详解Spring Batch:在Spring Boot中实现高效批处理
101 12
|
19天前
|
安全 Java 测试技术
详解Spring Profiles:在Spring Boot中实现环境配置管理
详解Spring Profiles:在Spring Boot中实现环境配置管理
62 10
|
16天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
59 5
|
1月前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
47 1
|
2月前
|
Java 测试技术 开发者
springboot学习四:Spring Boot profile多环境配置、devtools热部署
这篇文章主要介绍了如何在Spring Boot中进行多环境配置以及如何整合DevTools实现热部署,以提高开发效率。
97 2
|
2月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
167 1
|
2月前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
34 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
2月前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
87 0
|
2月前
|
Java Spring
springboot 学习十一:Spring Boot 优雅的集成 Lombok
这篇文章是关于如何在Spring Boot项目中集成Lombok,以简化JavaBean的编写,避免冗余代码,并提供了相关的配置步骤和常用注解的介绍。
117 0
|
4月前
|
运维 Java Nacos
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心