1. 程式人生 > >SpringMVC(4.x) 從搭建到放棄(含原始碼分析)——一

SpringMVC(4.x) 從搭建到放棄(含原始碼分析)——一

1. Spring MVC處理流程

DispatcherServlet -> 處理器對映 -(找到處理器)->
DispatcherServlet -> 控制器 -(邏輯檢視名+Model)-> DispatcherServlet -> 檢視解析器 -(渲染了Model資料的檢視View物件)-> 響應

Created with Raphaël 2.1.0請求請求DispatcherServletDispatcherServlet處理器對映處理器對映控制器控制器檢視解析器檢視解析器響應響應1. 請求被攔截2. 根據url尋找對應處理器3. 找到對應的控制器`getHandler(..)`
4. 交給控制器處理5. 返回邏輯檢視名和模型6. 找到配置的檢視解析器7. 渲染後的檢視`render(..)`8. 獲取響應

2. 配置Spring MVC

2.1 搭建專案

2.1.1 maven專案骨架

使用IDEA

File-New Module-Maven(勾選 create from archetype)-org.apache.maven.archetype:maven-archetype-webapp 然後next到底

使用Maven

$ cd YourWorkspace
$ mvn archetype:generate \
-DgroupId=com.mywebapp \
-DartifactId=mywebapp \ -Dversion=1.0.0 \ -DarchetypeArtifactId=maven-archetype-webapp \ -DarchetypeVersion=RELEASE \ -DinteractiveMode=false \ //不需要使用maven的互動模式來建立專案

2.1.2 pom.xml檔案

定義常量

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding
>
UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <org.slf4j-version>1.7.12</org.slf4j-version> <org.springframework-version>4.3.9.RELEASE</org.springframework-version> </properties>

引入spring MVC

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

引入thymleaf模板引擎

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

引入Servlet api和JSP api

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.1</version>
    <scope>provided</scope>
</dependency>
<!-- JSTL標籤庫 用於支援在jsp中使用標籤-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

引入JSR303 校驗庫(api 與 實現)

<dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.1.0.Final</version>
</dependency>
<!-- 其實和hibernate資料庫支援沒什麼關係 只是validation-api實現-->
<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.3.5.Final</version>
</dependency>

引入日誌系統

使用為SLF4J作為api log4j 1.x日誌系統為實現 jcl-over-slf4j為橋接器將使用其他日誌系統api(如apache commons-logging)的jar掉入到為SLF4J“陷阱”中 slf4j-log4j12為SLF4J與log4j相連線的介面

此處使用了log4j 1.x版本也是傳統使用最多的版本,但是現在log4j已經推出了2.x版本,並把專案進行了重構,增添了log4j對web的支援

<dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${org.slf4j-version}</version>
</dependency>
<dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${org.slf4j-version}</version>
      <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${org.slf4j-version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.16</version>
    <scope>runtime</scope>
</dependency>

或將log4j的實現改為2.x版本

<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-api</artifactId>  
    <version>2.5</version>
    <scope>runtime</scope>
</dependency>  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-core</artifactId>  
    <version>2.5</version>  
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-web</artifactId>
    <version>2.5</version>
    <scope>runtime</scope>
</dependency>

引入單元測試

<!-- JunitTest -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- 可以不需要啟動spring和web上下文來進行測試 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${org.springframework-version}</version>
    <scope>test</scope>
</dependency>
<!-- mock產生測試物件 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.22</version>
    <scope>test</scope>
</dependency>

2.1.3 目錄結構

專案原始檔目錄和專案目錄, 這兩者是不同的

原始檔目錄是組織原始碼的目錄結構, 一般如下

-mywebapp
--src/main/java                 //*.java
--src/main/resources            //*.properties或*.xml等配置檔案
--src/main/webapp               //web資源
----WEB-INF                     //web資源, 但瀏覽器不能用url方式直接訪問到
------jsp
------html
------template
------res/css
------res/js
------res/img
------web.xml                  //可以沒有哦!
--src/test/java
--src/test/resources
--target
--other dir you like

專案目錄是最終釋出到tomcat等容器上的目錄結構,即maven等工具編譯後的目錄結構

-mywebapp                       //所謂的web專案的根目錄 webroot
--META-INF
----MANIFEST.MF                 //記錄build工具打包等相關資料
--WEB-INF
----classes                     //專案自身的編譯後的src/main/java和src/main/resources的檔案 
                                //所謂的classpath的根目錄
----lib                         //專案所需的依賴
----jsp
----html
----template
----res/css
----res/js
----web.xml

2.2. 使用javaconfig配置web.xml

2.2.1 配置方法

一般的web專案都是要有web.xml 其作用是作為該web應用的上下文(Servlet Context)所需的配置檔案

在Servlet3.0環境中, 容器啟動時就會在類路徑下查詢實現javax.servlet.ServletContainerInitializer介面的類, Spring提供了這個類的實現org.springframework.web.SpringServletContainerInitializer, 在這個類中會反過來查詢實現了org.springframework.web.WebApplicationInitializer的類們並將配置任務交給它們來實現.

在Spring3.2中引入了一個便利的簡單WebApplicationInitializer基礎實現,
也就是AbstractAnnotationConfigDispatcherServletInitializer
(從名字上看它是一個用annotation配置DispatcherServlet的抽象類),
因此我們只要新建一個自定義配置類實現AbstractAnnotationConfigDispatcherServletInitializer即可

public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {RootConfig.class};  //相當於配置dao service bean的spring的xml門
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] {WebConfig.class}; //相當於只配置controller的spring的xml們
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"}; //DispatcherServlet的處理的url對映
    }
}

以上相當於

<listener>  
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener>
<context-param>  
    <param-name>contextConfigLocation</param-name>  
    <param-value>classpath:spring/applicationContext.xml</param-value>  
</context-param>

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- init-param 其實就是初始化這個bean需要注入的屬性-->
    <!--contextConfigLocation 是-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/dispatcher-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

注意<url-pattern>/*/的區別 /*會覆蓋掉所有的Servlet包括web容器(如Tomcat)中預定義的Servlet(如處理jsp 靜態資源), 可能會造成迴圈呼叫情況(dispatcher覆蓋處理jsp預設jsp servlet, dispatcher處理頁面渲染時轉移給預設jsp servlet, 而現在預設servlet就是dispatcher自身, 死迴圈), 而/只是覆蓋預設servlet(default servlet 該servlet就是處理其他servlet不處理的東西) 比如對於如/test.html(/*.html)的請求, 會自動找處理html的servlet處理, 如果沒配置就使用預設的處理html的sevlet, 尋找web根目錄下的test.html 記住 servlet不是鏈式呼叫, 屬於該servlet處理的內容如果找不到, 那麼它不會順著一個鏈尋找可以處理它的servlet

2.2.2 載入流程

  1. 容器呼叫SpringServletContainerInitialize.onStartup(Set<Class<?>> ServletContext)
  2. 查詢實現WebApplicationInitializer的類MyDispatcherServletInitializer呼叫onStartup(ServletContext)
  3. 該方法在抽象父類AbstractDispatcherServletInitializer
 public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        this.registerDispatcherServlet(servletContext);
    }
  1. 再呼叫父類的AbstractContextLoaderInitializer.onStartup(ServletContext)
public void onStartup(ServletContext servletContext) throws ServletException {
        this.registerContextLoaderListener(servletContext);
}

protected void registerContextLoaderListener(ServletContext servletContext) {
    // WebAppcationContext父類是spring的ApplicationContext, 其實就是web版的spring上下文
    // 呼叫子類AbstractAnnotationConfigDispatcherServletInitializer的createRootApplicationContext()
    WebApplicationContext rootAppContext = this.createRootApplicationContext();
    if (rootAppContext != null) {
        // 建立listener 放入上下文環境
        ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
        // ContextLoaderListener上下文環境的配置 
        listener.setContextInitializers(this.getRootApplicationContextInitializers());
        servletContext.addListener(listener);
    } else {
        this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
    }

}
  1. 回到AbstractDispatcherServletInitializer.onStartup(ServletContext)執行this.registerDispatcherServlet(servletContext)
protected void registerDispatcherServlet(ServletContext servletContext) {
    //...
    //呼叫子類AbstractAnnotationConfigDispatcherServletInitializer的createServletApplicationContext()
    //建立一個屬於DispatcherServlet的上下文環境
    WebApplicationContext servletAppContext = this.createServletApplicationContext();
    FrameworkServlet dispatcherServlet = this.createDispatcherServlet(servletAppContext);
    //dispatcherServlet上下文環境的配置
    dispatcherServlet.setContextInitializers(this.getServletApplicationContextInitializers());
    //在應用上下文中註冊dispatcherServlet
    Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
    Assert.notNull(registration, "Failed to register servlet with name '" + servletName + "'.Check if there is another servlet registered under the same name.");
    registration.setLoadOnStartup(1);
    registration.addMapping(this.getServletMappings());
    registration.setAsyncSupported(this.isAsyncSupported());
    //過濾器
    Filter[] filters = this.getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        Filter[] var7 = filters;
        int var8 = filters.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            Filter filter = var7[var9];
            //在應用上下文中註冊過濾器
            this.registerServletFilter(servletContext, filter);
        }
    }

    this.customizeRegistration(registration);
}
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        return new DispatcherServlet(servletAppContext);
}
  1. 執行一系列初始化DispatcherServlet的行為
public DispatcherServlet(WebApplicationContext webApplicationContext) {
    super(webApplicationContext);
    this.setDispatchOptionsRequest(true);
}

在以上流程中初始了兩個spring的上下文環境(WebApplicationContext 即ApplicationContext的子), 一個是ContextLoaderListener, 一個是DispatcherServlet(前者是後者的父環境)
this.getRootApplicationContextInitializers()this.getServletApplicationContextInitializers()返回的都是用@Configuration註解的類(配置spring bean的java config類) 一般ContextLoaderListener裡配置dao service或其他全域性的bean

2.2.3 上下文環境辨析

ServletContext

應用上下文環境, 代表了整個web應用(mywebapp), 由容器(如Tomcat)提供實現, 屬於javax.servlet.ServletContext, 可以說是“最頂層的上下文環境” 通常是用來在其中註冊listener servlet filter的(無論是用web.xml 還是java config方式), 它的啟動過程是最早的.

它有兩個地方可以放key-value
1. set/getInitParameter<context-param>裡設定的內容, 用於啟動
2. set/getAttribute 用於在webapp各個部分共享, 比如webapp的描述 名稱等

新增listener的作用是在servlet啟動 webapp還未完全執行前啟動一些過程,比如啟動spring 上下文

獲得ServletContext的方法
1. 在javax.servlet.Filter中直接獲取ServletContext context = config.getServletContext();

  1. 在HttpServlet中直接獲取this.getServletContext()

  2. 在其他地方r通過HttpSession獲取session.getServletContext(); 或通過HttpServletRequest獲取request.getSession().getServletContext();

  3. 在thymeleaf模板直接${application}(SpEL 模板引擎)或${#servletContext}(OGNL 原生語言)

ServletContext裡新增屬性servletContext.setAttribute(name, value)

spring 上下文環境

一般指的就是ContextLoaderListener的WebApplicationContext
ContextLoaderListenercontextInitialized(ServletContextEvent)被呼叫
執行它父類ContextLoader.initWebApplicationContext(ServletContext)時它會在被直接新增到ServletContext的attribute屬性中

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

可通過WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)方法獲取到該上下文

dispatcherServlet 上下文

指的是DispatcherServlet的WebApplicationContext

  1. 由於設定了registration.setLoadOnStartup(1); 在容器啟動完成後就呼叫servlet的init() DispatcherServlet 繼承FrameworkServlet繼承HttpServletBean繼承 HttpServletHttpServletBean實現了init()
public final void init() throws ServletException {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Initializing servlet '" + this.getServletName() + "'");
    }

    PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
            this.initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        } catch (BeansException var4) {
            if (this.logger.isErrorEnabled()) {
                this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
            }

            throw var4;
        }
    }
    // 呼叫子類FrameworkServlet的initServletBean()
    this.initServletBean();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Servlet '" + this.getServletName() + "' configured successfully");
    }

}
  1. FrameworkServletinitServletBean()中呼叫了initWebApplicationContext() 將ContextLoaderListener中搞定的spring上下文設定為Dispatcher中上下文的父容器
protected WebApplicationContext initWebApplicationContext() {
    //...
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    //...
    cwac.setParent(rootContext);
    //...
}