Spring中對於WebApplicationInitializer的理解

1、前言

《SpringMVC學習(五)——零配置實現SpringMVC》這篇文章中我們沒有使用Spring的配置實現了一個正常的SpringMVC的功能,裏面核心的一個點就是使用了WebApplicationInitializer,那這篇文章就詳細說明一下這個接口的作用。

2、WebApplicationInitializer的定義

從起初的Spring配置文件,到後來的Spring支持註解到後來的SpringBoot,Spring框架在一步步的使用註解的方式來去除Spring的配置的發展過程。WebApplicationInitializer就是取代web.xml配置的一個接口。

public interface WebApplicationInitializer {
    void onStartup(ServletContext var1) throws ServletException;
}

通過覆蓋接口提供的onStartup方法我們可以往Servlet容器裏面添加我們需要的servletlistener等,並且在Servlet容器啓動的過程中就會加載這個接口的實現類,從而起到和web.xml相同的中作用,從而可以替代以前在web.xml中所做的配置。

3、實現原理

我們首先可以從Spring源碼中找到SpringServletContainerInitializer實現類。

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

    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        LinkedList initializers = new LinkedList();
        Iterator var4;
        if(webAppInitializerClasses != null) {
            var4 = webAppInitializerClasses.iterator();

            while(var4.hasNext()) {
                Class initializer = (Class)var4.next();
                if(!initializer.isInterface() && !Modifier.isAbstract(initializer.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(initializer)) {
                    try {
                        initializers.add((WebApplicationInitializer)initializer.newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        if(initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            var4 = initializers.iterator();

            while(var4.hasNext()) {
                WebApplicationInitializer initializer1 = (WebApplicationInitializer)var4.next();
                initializer1.onStartup(servletContext);
            }

        }
    }
}

這個類上面有@HandlesTypes({WebApplicationInitializer.class})這個註解,這個註解的作用是將其value中配置的一些類放入到ServletContainerInitializer

initializers.add((WebApplicationInitializer)initializer.newInstance());

最後通過循環去執行WebApplicationInitializer中的onStartup方法來實現裏面的具體的具體的邏輯。

while(var4.hasNext()) {
	WebApplicationInitializer initializer1 = (WebApplicationInitializer)var4.next();
	initializer1.onStartup(servletContext);
}

那問題來了,Tomcat容器怎麼知道先執行這個SpringServletContainerInitializer類?
這裏涉及一個知識點SPI機制SPI全稱爲 Service Provider Interface,是一種服務發現機制。SPI機制是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI機制爲我們的程序提供拓展功能。

Tomcat啓動過程中查找所有的ServletContainerInitializer實現類然後添加到StandardContext的initializers集合中,然後執行裏面的onStartup方法。

Spring中SPI就是通過SpringServletContainerInitializer類來實現的
sci機制
關於Web應用的啓動過程在《一個基於註解配置的Web項目的啓動流程分析》這篇文章寫的很好。

4、利用SPI我們能做什麼?

可以加一些自己的啓動配置信息,把自己的Servlet打成jar包放到Tomcat服務器或者其他工程中執行。我們可以實現一個自己的SPI接口。

4.1、定義一個MyWebAppInitializer
public interface MyWebAppInitializer {
    void loadOnStart(ServletContext var1) throws ServletException;
}
4.2、定義一個MySpringServletContainerInitializer
  1. 實現ServletContainerInitializer接口
  2. 修改@HandlesTypes爲我們自己定義的MyWebAppInitializer.class
  3. 實例化MyWebAppInitializer的實現類,並且調用接口的loadOnStart方法
@HandlesTypes(MyWebAppInitializer.class)
public class MySpringServletContainerInitializer implements ServletContainerInitializer{
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletcontext)
            throws ServletException {
        if (set != null) {
            for (Class<?> waiClass : set) {
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        MyWebAppInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        //創建MyWebAppInitializer實現類的對象,並調用loadOnStart方法
                        ((MyWebAppInitializer) waiClass.newInstance()).loadOnStart(servletcontext);
                    } catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }
    }
}
4.3、寫一個MyWebAppInitializer的實現類
public class MyWebApplicationInitializerTest implements MyWebAppInitializer{
    @Override
    public void loadOnStart(ServletContext servletContext){
        System.out.println("啓動執行MyWebApplicationInitializerTest的loadOnStart方法");
        //註冊一個爲名字call的servlet
        ServletRegistration.Dynamic servletReg = servletContext.addServlet("call", CallServlet.class);
        servletReg.setLoadOnStartup(1);
        servletReg.addMapping("/call");
    }
}

其中裏面的CallServlet.java如下

public class CallServlet extends HttpServlet {
    private static final long serialVersionUID = 3684613967452881093L;

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        resp.getWriter().write(name + ", if you like me, please call me!");
    }
}
4.4、添加配置文件javax.servlet.ServletContainerInitializerr

添加位置爲:src -> main -> resources-> META-INF-> services ->javax.servlet.ServletContainerInitializerr

內容:com.leo.spi.MySpringServletContainerInitializer
位置

4.5、啓動項目測試

啓動日誌:

啓動執行MyWebApplicationInitializerTest的loadOnStart方法

瀏覽器測試:http://localhost:8080/springmvc/call?name=leo825
測試成功
上面也提到了,可以把這個SPI的方式打成jar包在其他項目或者直接在Tomcat容器這種運行。
本文的相關源碼請參考:chapter-5-springmvc-zero-configuration
spi代碼包路徑:com.leo.spi
https://gitee.com/leo825/spring-framework-learning-example.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章