SpringMVC启动原理(API版)

ServletContainerInitializer

在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册 servlet 或者 filtes 等, servlet规范 中通过 ServletContainerInitializer 实现此功能。

每个框架要使用 ServletContainerInitializer 就必须在对应的jar包的 META-INF/services 目录创建一个名为 javax.servlet.ServletContainerInitializer 的文件,文件内容指定具体的 ServletContainerInitializer 实现类,

那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

一般伴随着 ServletContainerInitializer 一起使用的还有 HandlesTypes 注解,通过 HandlesTypes 可以将感兴趣的一些类注入到 ServletContainerInitializerdeonStartup 方法作为参数传入。

Tomcat容器的 ServletContainerInitializer 机制的实现,主要交由 Context容器ContextConfig监听器 共同实现,

ContextConfig监听器负责在容器启动时读取每个web应用的 WEB-INF/lib 目录下包含的jar包的 META-INF/services/javax.servlet.ServletContainerInitializer ,以及 web根目录 下的 META-INF/services/javax.servlet.ServletContainerInitializer

通过反射完成这些 ServletContainerInitializer 的实例化,然后再设置到Context容器中,最后Context容器启动时就会分别调用每个 ServletContainerInitializeronStartup 方法,并将感兴趣的类作为参数传入。

image.png

基本的实现机制如图,首先通过 ContextConfig监听器 遍历每个jar包或web根目录的 META-INF/services/javax.servlet.ServletContainerInitializer 文件,根据读到的类路径实例化每个 ServletContainerInitializer ;然后再分别将实例化好的 ServletContainerInitializer 设置进Context容器中;最后Context容器启动时分别调用所有 ServletContainerInitializer 对象的 onStartup 方法。

假如读出来的内容为 com.seaboat.mytomcat.CustomServletContainerInitializer ,则通过反射实例化一个 CustomServletContainerInitializer 对象,这里涉及到一个 @HandlesTypes 注解的处理,被它标明的类需要作为参数值传入到 onStartup 方法。

如下例子:

@HandlesTypes({ HttpServlet.class,Filter.class }) 
public class CustomServletContainerInitializer implements 
    ServletContainerInitializer { 
  public void onStartup(Set<Class> classes, ServletContext servletContext) 
      throws ServletException {
      for(Class c : classes) 
         System.out.println(c.getName());
  } 
}

其中 @HandlesTypes 标明的 HttpServletFilter 两个class被注入到了 onStartup 方法。

所以 这个注解也是需要在ContextConfig监听器中处理

前面已经介绍了注解的实现原理,由于有了编译器的协助,我们可以方便地通过 ServletContainerInitializer 的class对象中获取到 HandlesTypes 对象,进而再获取到注解声明的类数组,如

HandlesTypes ht =servletContainerInitializer.getClass().getAnnotation(HandlesTypes.class);
Class[] types = ht.value();

即可获取到 HttpServletFilter 的class对象数组,后面Context容器调用 CustomServletContainerInitializer 对象的 onStartup 方法时作为参数传入。

至此,即完成了servlet规范的 ServletContainerInitializer 初始化器机制。

SpringServletContainerInitializer

上面提到了 META-INF/services/javax.servlet.ServletContainerInitializer ,在Spring spring-web-4.3.0.RELEASE.jar Jar包中可以找到该文件,内容如下:

org.springframework.web.SpringServletContainerInitializer

下面,我们就来详细讲解下 SpringServletContainerInitializer

首先看下API中的描述:

Servlet 3.0 ServletContainerInitializer 被设计为使用Spring的 WebApplicationInitializer SPI来支持Servlet容器的 基于代码 的配置,而不是传统的基于web.xml的配置(也可能两者结合使用)。

一、运作机制

假设类路径中存在 spring-web 模块的JAR包, SpringServletContainerInitializer 将被加载并实例化,并且在容器启动期间由Servlet 3.0容器调用 onStartup 方法。

这是通过JAR Services API ServiceLoader.load(Class) 方法(检测Spring-Web模块的 META-INF/services/javax.servlet.ServletContainerInitializer 配置文件)实现的。

二、与web.xml结合使用

Web应用程序可以选择通过 web.xml 中的 metadata-complete 属性(它控制扫描Servlet注解的行为)

或通过 web.xml 中的 元素(它控制哪些 web fragments (i.e. jars)被允许执行扫描 ServletContainerInitializer )

来限制Servlet容器在启动时扫描的类路径。

当使用这个特性时,可以通过添加”spring_web”到 web.xml 里的 web fragments 列表来启用 SpringServletContainerInitializer

如下所示:

    some_web_fragment
    spring_web

servlet3.X中的metadata-complete属性

在Servlet3.X的web.xml中可以设置metadata-complete属性,例如:

如果设置 metadata-complete="true" ,会在启动时不扫描注解(annotation)。如果不扫描注解的话,用注解进行的配置就无法生效,例如: @WebServlet

三、与Spring的 WebApplicationInitializer 的关系

Spring的WebApplicationInitializer SPI仅由一个方法组成: WebApplicationInitializer.onStartup(ServletContext) 。声明与 ServletContainerInitializer.onStartup(Set, ServletContext) 非常相似:简单地说, SpringServletContainerInitializer 负责将 ServletContext 实例化并委托给用户定义的 WebApplicationInitializer 实现。然后 每个 WebApplicationInitializer 负责完成初始化 ServletContext 的实际工作 。下面的 onStartup 文档中详细介绍了委托的具体过程。

四、注意事项

一般来说,这个类应该被视为 WebApplicationInitializer SPI的支持。利用这个 容器初始化器 也是完全 可选 的:虽然这个初始化器在所有的Servlet 3.0+运行环境下被加载和调用,但用户可以选择是否提供 WebApplicationInitializer 实现。如果未检测到 WebApplicationInitializer 类型,则此 SpringServletContainerInitializer 将不起作用。

请注意,除了这些类型是在 spring-web 模块JAR中提供的,使用这个 SpringServletContainerInitializerWebApplicationInitializer 与Spring MVC没有任何“捆绑”。相反,它们可以被认为是通用的,以便于简化 ServletContext 基于代码的配置。换句话说,任何 servlet , listener , 或者 filter 都可以在 WebApplicationInitializer 中注册,而不仅仅是Spring MVC特定的组件。

SpringServletContainerInitializer 既不是为扩展而设计的。它应该被认为是一个内部类型, WebApplicationInitializer 是面向用户的SPI。

好啦,现在对 SpringServletContainerInitializer 有了一个比较透彻的了解,下面我们来看一下唯一的 onStartup 方法。

ServletContext 委托给类路径中的 WebApplicationInitializer 实现。

因为这个类声明了 @HandlesTypes(WebApplicationInitializer.class) ,所以

Servlet 3.0+容器会自动扫描类路径下Spring的 WebApplicationInitializer 接口的实现,并将所有这些类型的集合提供给这个方法的 webAppInitializerClasses 参数。

如果在类路径下找不到 WebApplicationInitializer 实现,则此方法不会有任何操作。将发出INFO级别的日志消息,通知用户 ServletContainerInitializer 确实已被调用,但没有找到 WebApplicationInitializer 实现。

假设检测到一个或多个 WebApplicationInitializer 类型,它们将被实例化(如果存在 @Order 注释或实现 Ordered 接口,则对其进行排序)。然后,将调用每个实例 WebApplicationInitializer.onStartup(ServletContext) 方法,并委派 ServletContext ,以便每个实例都可以注册和配置Servlet,例如Spring的 DispatcherServlet ,listeners(如Spring的 ContextLoaderListener ),或者其他Servlet API组件(如filters)。

下面是 SpringServletContainerInitializer 的源码:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        // WebApplicationInitializer实现如果存在`@Order`注释或实现`Ordered`接口,则对其进行排序,故这里使用LinkedList
        List initializers = new LinkedList();

        if (webAppInitializerClasses != null) {
            for (Class waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                // 有时候,Servlet容器提供给我们的可能是无效的webAppInitializerClass
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer) waiClass.newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

WebApplicationInitializer

下面我们来看下 WebApplicationInitializer API文档的相关介绍。

在Servlet 3.0+环境中实现该接口,以便以编程方式配置 ServletContext ,而不是以传统的基于web.xml的方法。 WebApplicationInitializer SPI的实现将被 SpringServletContainerInitializer (它本身是由Servlet 3.0容器自动引导的)自动检测到。

Example

基于XML的方式

大多数Spring用户构建Web应用程序时需要注册Spring的 DispatcherServlet 。作为参考,通常在 WEB-INF/web.xml 中按如下方式:

    dispatcher
    
      org.springframework.web.servlet.DispatcherServlet
    
    
      contextConfigLocation
      /WEB-INF/spring/dispatcher-config.xml
    
    1
  
 
  
    dispatcher
    /
  

基于代码的方式

DispatcherServlet 注册逻辑与上述等效

public class MyWebAppInitializer implements WebApplicationInitializer {
  
      @Override
      public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
  
        ServletRegistration.Dynamic dispatcher =
          container.addServlet("dispatcher", new DispatcherServlet(appContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
      }
   }

作为上述的替代方法,您还可以继承自 org.springframework.web.servlet.support.AbstractDispatcherServletInitializer

正如您所看到的,使用Servlet 3.0的 ServletContext.addServlet 方法,我们注册了一个 DispatcherServlet 的实例。

这种风格简单明了。不用关心处理 init-params 等,只是普通的JavaBean风格的属性和构造函数参数。在将其注入到 DispatcherServlet 之前,您可以根据需要自由创建和使用Spring应用程序上下文。

大多数Spring Web组件已经更新,以支持这种注册方式。你会发现 DispatcherServletFrameworkServletContextLoaderListenerDelegatingFilterProxy 现在都支持构造函数参数。Servlet 3.0 ServletContext API允许以编程方式设置 init-paramscontext-params 等。

完全基于代码的配置方法

在上面的例子中, WEB-INF/web.xmlWebApplicationInitializer 形式的代码替换,但 dispatcher-config.xml 配置仍然是基于XML的。 WebApplicationInitializer 非常适合与Spring的基于代码的 @Configuration 类一起使用。以下示例演示了使用Spring的 AnnotationConfigWebApplicationContext 代替 XmlWebApplicationContext 进行重构,以及使用用户定义的 @ConfigurationAppConfigDispatcherConfig ,而不是Spring XML文件。这个例子也超出了上面的例子来演示’root’ 应用上下文的典型配置和 ContextLoaderListener 的注册:

public class MyWebAppInitializer implements WebApplicationInitializer {
  
      @Override
      public void onStartup(ServletContext container) {
        // Create the 'root' Spring application context
        AnnotationConfigWebApplicationContext rootContext =
          new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);
  
        // Manage the lifecycle of the root application context
        container.addListener(new ContextLoaderListener(rootContext));
  
        // Create the dispatcher servlet's Spring application context
        AnnotationConfigWebApplicationContext dispatcherContext =
          new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(DispatcherConfig.class);
  
        // Register and map the dispatcher servlet
        ServletRegistration.Dynamic dispatcher =
          container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
      }
  
   }

作为上述的替代方法,您还可以继承自 org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer 。注意, WebApplicationInitializer 的实现类会被自动检测到。

Ordering WebApplicationInitializer execution

WebApplicationInitializer 实现类可以有选择地在类上使用Spring的 @Order 注解,也可以实现Spring的 Ordered 接口。如果是这样,初始化程序将在调用之前排序。这为用户提供了确保Servlet容器初始化顺序的机制。使用此功能的情况很少,因为典型的应用程序可能会将所有容器初始化集中在一个 WebApplicationInitializer 中。

注意事项

web.xml版本

WEB-INF/web.xmlWebApplicationInitializer 的使用不是互斥的; 例如, web.xml 可以注册一个 servlet ,而 WebApplicationInitializer 可以注册另一个。 Initializer 甚至可以通过诸如 ServletContext.getServletRegistration(String) 之类的方法来修改在 web.xml 中执行的注册。但是,如果应用程序中存在 WEB-INF/web.xml ,则其版本属性必须设置为”3.0″或更高,否则Servlet容器将忽略 ServletContainerInitializer 的引导。

下面我们来看一组 WebApplicationInitializer 的实现类:

继承关系如下:

AbstractAnnotationConfigDispatcherServletInitializer
        |
        | —— AbstractDispatcherServletInitializer
                   |
                   | —— AbstractContextLoaderInitializer
                             |
                             | ——  WebApplicationInitializer

AbstractAnnotationConfigDispatcherServletInitializer

org.springframework.web.WebApplicationInitializer 实现类的基类,用于注册配置了 @Configuration/@Component 注解标记的配置类 DispatcherServlet

具体的实现类需要实现 getRootConfigClasses()getServletConfigClasses() 以及 getServletMappings() 方法。更多的方法由 AbstractDispatcherServletInitializer 提供。这是使用基于Java配置应用程序的首选方法。

下面是 AbstractAnnotationConfigDispatcherServletInitializer 的源码:

public abstract class AbstractAnnotationConfigDispatcherServletInitializer
        extends AbstractDispatcherServletInitializer {

    /**
     * 创建要提供给`ContextLoaderListener`的**根应用程序上下文**。
     * 

* 返回的上下文委托给`ContextLoaderListener.ContextLoaderListener(WebApplicationContext)`, * 并将作为`DispatcherServlet`应用程序上下文的父上下文来建立。 *

* 因此,它通常包含中间层服务,数据源等。 *

* 该方法创建一个`AnnotationConfigWebApplicationContext`,并为其提供由`getRootConfigClasses()`返回的配置类。如果`getRootConfigClasses()`返回Null,则不会创建根应用上下文。 */ @Override protected WebApplicationContext createRootApplicationContext() { Class[] configClasses = getRootConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext(); rootAppContext.register(configClasses); return rootAppContext; } else { return null; } } /** * 创建一个**Servlet应用上下文**以提供给`DispatcherServlet`。 *

* 返回的上下文被委托给Spring的`DispatcherServlet.DispatcherServlet(WebApplicationContext)`方法。 *

* 因此,它通常包含控制器,视图解析器,locale解析器和其他Web相关的bean。 *

* 该实现创建一个`AnnotationConfigWebApplicationContext`,为其提供由`getServletConfigClasses()`返回的配置类。 */ @Override protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext(); Class[] configClasses = getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { servletAppContext.register(configClasses); } return servletAppContext; } /** * 指定要提供给根应用上下文的`@Configuration`或`@Component`注解标记的配置类。 */ protected abstract Class[] getRootConfigClasses(); /** * 指定要提供给Dispatcher Servlet应用上下文的`@Configuration`或`@Component`注解标记的配置类。 */ protected abstract Class[] getServletConfigClasses(); }

简书责编内容来自:简书 (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合编程 » SpringMVC启动原理(API版)

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录