如何向一個WebApp引入Spring與Spring MVC

如何向一個WebApp引入Spring與Spring MVC

1

在Servlet 3.0環境中,容器(加載運行webapp的軟件,如Tomcat)會在類路徑中查找實現javax.servlet.ServletContainerInitializer接口的類(這一行爲本質上是Java EE標準和協定所要求的,Tomcat是基於該協定的一種實現),如果能發現的話,就會用它來配置Servlet容器。

Spring提供了這個接口的實現,名爲SpringServletContainerInitializer,因此一個引入的SringMVC的web項目在沒有其它設置的情況下會被Tomcat找到SpringServletContainerInitializer。

SpringServletContainerInitializer

2

SpringServletContainerInitializer又會查找實現WebApplicationInitializer接口的類並調用其onStartup(ServletContext servletContext)方法,其中ServletContext對象由其負責將服務器生成的唯一的ServletContext實例傳入。

WebApplicationInitializer

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach.

ServletContext

Defines a set of methods that a servlet uses to communicate with its servlet container, for example,

ServletContextMethods

階段性總結

到目前位置,我們已經可以使用SpringMVC來增設Servlet了,雖然這看起來並不美觀也不簡便。代碼如下所示。

package spittr.config;

import org.springframework.web.WebApplicationInitializer;
import spittr.web.AServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //增加一個Servelt 其中AServlet是Servlet接口的實現類,我的實現直接繼承了HttpServlet
        ServletRegistration.Dynamic aServlet = servletContext.addServlet("AServlet", AServlet.class);
        //爲AServlet增設映射路徑,其作用等同於@WebServlet(urlPatterns={"/AServlet"})
        aServlet.addMapping(new String[]{"/AServlet"});
    }
}
package spittr.web;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=utf-8");

        PrintWriter writer = resp.getWriter();

        writer.write("我收到了你的GET");
    }
}

在瀏覽器中訪問項目名/AServlet可以直接得到信息:我收到了你的GET。

然而,這樣的實現在美觀和便利上還不如使用Servlet3.0引入和更新的@WebServlet等機制,何必要用SpringMVC呢?

@WebServlet(urlPatterns={"/AServlet"})
public class AServlet extends HttpServlet {
...
}

實際上如果在不明原理前已經有過些許實踐,其實很容易想到答案——我們要利用SpringMVC集成的Spring的優秀特性(比如對自動掃描和註冊bean,管理bean的Spring容器等)以及SpringMVC,@RequestMapping這些由SpringMVC實現的高效美觀的功能。

3

首先肯定是在我們的webapp中繼承Spring,也就是引入一個Spring容器並做一些配置。

這很簡單,在onStartup中實例化一個ApplicationContext的實例即可。查詢ApplicationContext的javadoc,看到目前所有的ApplicationContext實現類:

All Known Implementing Classes:
AbstractApplicationContext, AbstractRefreshableApplicationContext, AbstractRefreshableConfigApplicationContext, AbstractRefreshableWebApplicationContext, AbstractXmlApplicationContext, AnnotationConfigApplicationContext, AnnotationConfigWebApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, GenericApplicationContext, GenericGroovyApplicationContext, GenericWebApplicationContext, GenericXmlApplicationContext, GroovyWebApplicationContext, ResourceAdapterApplicationContext, StaticApplicationContext, StaticWebApplicationContext, XmlWebApplicationContext

而我們打算使用基於Java代碼的配置並開啓基於註解的自動掃描,同時應用場景爲webapp,所以應該使用AnnotationConfigWebApplicationContext實現類。

綜上所述,可以得到如下代碼:

package spittr.config;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.WebApplicationInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(AppConfig.class);
    }
}
@Configuration
@ComponentScan("spittr.web")
public class AppConfig {
}

至此,我們已經在這個webapp中集成了Spring容器,從理論上講,我們應該可以對一個Servlet標註@Controller後使其自動被註冊和使用。但是由於@RequestMapping我們還不知道能不能用,實際上無法對其進行測試(因爲即便將服務器註冊到了Spring容器中,我們也無法爲它配置映射路徑)。

那麼現在就該去解決@RequestMapping了。傳送門

4

@RequestMapping javadoc文件對這一註解做了如下解讀

Annotation for mapping web requests onto methods in request-handling classes with flexible method signatures.

Both Spring MVC and Spring WebFlux support this annotation through a RequestMappingHandlerMapping and RequestMappingHandlerAdapter in their respective modules and package structure. For the exact list of supported handler method arguments and return types in each, please use the reference documentation links below:

Note: This annotation can be used both at the class and at the method level. In most cases, at the method level applications will prefer to use one of the HTTP method specific variants @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, or @PatchMapping.

NOTE: When using controller interfaces (e.g. for AOP proxying), make sure to consistently put all your mapping annotations - such as @RequestMapping and @SessionAttributes - on the controller interface rather than on the implementation class.

其中最重要的在第二段,它說明了Spring MVC通過使用RequestMappingHandlerMappingRequestMappingHandlerAdapter 得以支持@RequestMappin註解。

javadoc:RequestMappingHandlerMapping

javadoc:RequestMappingHandlerAdapter

可以發現,這兩個類都是可以被實例化的,且構造器不需要參數。

既然如此,我們可以試着在AppConfig中配置這兩個類。

@Configuration
@ComponentScan("spittr.web")
public class AppConfig {
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(){
        return new RequestMappingHandlerAdapter();
    }
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping(){
        return new RequestMappingHandlerMapping();
    }
}

然後使用帶@Controller和@RequestMapping的類

package spittr.web;

@Controller
@RequestMapping("/BServlet")
public class BServlet{
    @RequestMapping(method = RequestMethod.GET)
    public void doGet() {
        System.out.println("BServlet:我收到了你的GET");
    }
}

不過測試結果是糟糕的,我們沒有如願實現訪問BServlet。

失敗的原因沒有官方文檔,但結合之後進一步的學習,不難猜測理由應該是:我們AppConfig的Spring-beans容器其實沒有和服務器軟件(Tomcat)結合起來。我們只是在onStartUp方法中實例化了一個Spring-beans容器,甚至可以認爲在方法的生命週期結束之後,這個實例就直接沒了。如若真的如此,我們就連實際上把Spring集成到這個WebApp中都沒有做到,怎麼可能做到開啓Spring MVC註解呢。

5

事已至此,就只能閱讀官方文檔了。官方文檔

開門見山地:

Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components. This model is flexible and supports diverse workflows.

大意:Spring MVC圍繞一個前線控制器模式(front controller pattern)而設計,在這種模式下一個核心Servlet,也就是DispatchereServlet1(由Spring實現的Servlet類),會爲處理客戶端請求提供了算法,而真正的工作(處理請求)由可配置的代理組件來執行。

因此可以認爲,要充分利用SpringMVC,必然要加載SpringMVC自行實現的Servlet類:org.springframework.web.servlet.DispatcherServlet

官方文檔給出了一段初始化代碼:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        //AppConfig是自定義的帶@Configuration註解的類
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        // 將Spring容器與DispatcherServlet綁定
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

這段代碼的前半部分,我們是很熟悉的。第三章就做過。

這段代碼的後半部分其實沒有什麼新意,但下半部分的第一行非常關鍵

DispatcherServlet servlet = new DispatcherServlet(ac);

接受一個AnnotationConfigWebApplicationContext作爲構造器參數!這實際上解決了我們在第四章測試失敗後反思的可能的疑惑——我們配置的Spring容器實際上並沒有和tomcat融合起來。

那麼現在,將官方代碼中的ac換成我們自己的,是不是就能成功了呢?不妨一試

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);

        DispatcherServlet dispatcher = new DispatcherServlet(ac);
        ServletRegistration.Dynamic d = servletContext.addServlet("dispatcher", dispatcher);
        d.setLoadOnStartup(1);
        d.addMapping("/");
    }
}
/*
AppConfig
BServlet
相較之前完全沒有變化,所以不展示
*/

結果是喜人的,我們嘗試成功了。在控制檯上可以看到輸出BServlet:我收到了你的GET

官方文檔進一步說明:

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml. In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

這段話應該分成這兩個部分:

  • The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml.The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

    這一部分上來先說,DispatcherServlet就像任何Servlet一樣,也是需要做好聲明和映射的。下面的代碼介紹了使用Servlet container提供的自動探測註冊功能來註冊和初始化DispatcherServlet。這裏所謂的Servlet container的自動探測,其實就是指之前提到的1,2兩個階段。

  • In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

    這一部分說,DispatcherServlet被配置註冊好之後,也可以反過來使用Spring配置來發現和委派爲它爲請求映射,視圖渲染,異常處理所需要的組件。
    那麼,DispatcherServlet要如何反過來配置它自己的組件呢?帶着這一疑問,我們繼續往下看。

6

官方文檔緊接着提到了一個WebApplicationInitializer的Spring實現類AbstractAnnotationConfigDispatcherServletInitializer,它可以避免直接使用ServletContext(它自己已經用了),通過重寫特定的方法完成配置。

In addition to using the ServletContext API directly, you can also extendAbstractAnnotationConfigDispatcherServletInitializer and override specific methods (see the example under Context Hierarchy).

跟隨Context Hierarchy超鏈接一探究竟。先放上example code:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }
}

再看文字說明

DispatcherServlet expects a WebApplicationContext (an extension of a plain ApplicationContext) for its own configuration.

DispatcherServlet爲它自己的配置需要一個WebApplicationContext(ApplicationContext的子接口)即一個Spring容器的配置實現類。

WebApplicationContext has a link to the ServletContext and the Servlet with which it is associated. It is also bound to the ServletContext such that applications can use static methods on RequestContextUtils to look up the WebApplicationContextif they need access to it.

一個Spring容器與ServletContext和與它共生的Servlet又關聯。這個Spring容器因爲綁定ServletContext,所以也可以通過類RequestContextUtils的靜態方法去得到。

For many applications, having a single WebApplicationContext is simple and suffices. It is also possible to have a context hierarchy where one root WebApplicationContext is shared across multiple DispatcherServlet (or other Servlet) instances, each with its own child WebApplicationContext configuration. See Additional Capabilities of the ApplicationContext for more on the context hierarchy feature.

絕大部分應用來說,一個Spring容器就夠用了。但也可以有一個有層級的容器結構——一個根Spring容器在多個(全部)Servlet實例中共享,同時每個Servlet實例也有自己的WebApplicationContext配置。

Java EE和Servlet3.0標準的Servlet接口其實是不支持Servlet實例共生一個ApplicationContext的,因爲後者畢竟是Spring的專屬。所以這裏的Servlet實例考慮爲像DispatcherServlet這樣由Spring實現並提供的類,而不包括用戶自定義的符合Java EE和Servlet3.0標準的Servlet接口的Servlet。

The root WebApplicationContext typically contains infrastructure beans, such as data repositories and business services that need to be shared across multiple Servlet instances. Those beans are effectively inherited and can be overridden (that is, re-declared) in the Servlet-specific child WebApplicationContext, which typically contains beans local to the given Servlet.

在層級話的Spring容器結構中,根Spring容器通常包含基礎設施的組件,比如數據持久化層,商業服務層這種需要在各種Servlet中共享的組件。這些組件能夠被有效地繼承地同時,也可以被在Servlet相關的子Spring容器中被重新配置,使得組件可以針對給定的Servlet因地制宜。

到這裏再回看代碼。

    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

顯然,這裏的RootConfig.class是用戶自定義的帶@Configuration註解的Spring容器配置類,用以實現根Spring容器。

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }

這個就是AbstractAnnotationConfigDispatcherServletInitializer默認實現的那個DispatcherServlet的伴生Spring容器配置。

    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }

這個則是確定AbstractAnnotationConfigDispatcherServletInitializer默認實現的那個DispatcherServlet所要管理的request URI映射。

至此1.1.1 Context Hierarchy結束,我們之前就是根據超鏈接跳到這一章節的,這一章節結束後,我們返回之前的位置繼續閱讀文檔。

發現緊接着就又是1.1.1 Context Hierarchy,直接跳過讀下一章。

7

1.1.2. Special Bean Types

The DispatcherServlet delegates special beans to process requests and render the appropriate responses. By “special beans” we mean Spring-managed Object instances that implement framework contracts. Those usually come with built-in contracts, but you can customize their properties and extend or replace them.

1.1.3. Web MVC Config

Applications can declare the infrastructure beans listed in Special Bean Types that are required to process requests. The DispatcherServlet checks the WebApplicationContext for each special bean. If there are no matching bean types, it falls back on the default types listed in DispatcherServlet.properties.

In most cases, the MVC Config is the best starting point. It declares the required beans in either Java or XML and provides a higher-level configuration callback API to customize it.

這兩個部分回答了我們的問題——DispatcherServlet要如何反過來配置它自己的組件——DispatcherServlet將會搜索它可以訪問的WebApplicationContext(這包括根Spring容器和它自己伴生的子Spring容器)來查找每個special bean——即被委派來處理請求渲染迴應等工作的組件——的設置。如果沒有的話,它將使用默認的,保存在DispatcherServlet.properties中的設定。

總結

到這裏,對於如何將Spring和Spring MVC集成到一個WebApp中的過程以及爲什麼可以集成進來已經分析得差不多了。

更進一步得學習Spring MVC,就繼續仔細閱讀官方文檔吧!


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