WebApplicationContext 中特殊的 bean 型別(一)— 請求/異常處理

biubiubiubiubiubiubiu發表於2019-01-23

前言

其實 Spring 的基本思想就是“萬物都是 bean”,那麼為了滿足 spring 工程的需要,spring 中有一些預設的 bean 選項,它們用於處理請求,渲染檢視等。比如上一篇文章就用過的 viewResolver 的配置。當然,servlet 也允許你配置使用不同特定的 bean,但是,如果你沒有配置,spring 將會按照預設的 bean 進行配置。本章將會詳細說明文件中列出的 bean 的配置以及具體的使用例子,所講述的 bean 型別包括:

  • HandlerMapping 和 HandlerAdapter
  • HandlerExceptionResolver
  • LocaleResolver & LocaleContextResolver
  • ThemeResolver
  • MultipartResolver

HandlerAdapter 和 HandlerMapping 解析

前期準備

本章節將基於文件實踐(一)的程式碼進行後續的操作,因此我們使用了單個 ContextConfig 來配置工程 Context 物件,也就是 root-context.xml 檔案。另一方面,為了實現 HandlerMapping 在 xml 配置的功能,我們關掉了

<mvc:annotation-driven/>
複製程式碼

的功能,使得 @Controller 註解下的類不再會被自動配置並且做 url 的對映,現在再去試一下 localhost:8080/hello.do 的話,已經是 404 Not Found 了。之後再進行後續的實踐過程。

這裡 HandlerMapping 和 HandlerAdapter 一起講是因為,HandlerMapping 需要 HandlerAdapter 的支援才能正常執行。HandlerMapping 用於將請求的 url 對映到對應的 controller 上面,如果沒有進行配置的話,@Controller 註解即為 HandlerMapping,上一篇的 ExampleController 即有著和上述相似的功能。值得注意的是,Spring MVC 4.0 之後主推 Annotation Driven,也就是註解驅動模式下的工程,因此,對應的 adapter 已經標記為 deprecated,不推薦使用,這裡只做幫助理解使用。

HandlerAdapter

由於工程中的 Controller 都是用註解配置的,因此,在 DispatcherServlet 根據 bean 的配置資訊(root-context.xml,我們用 Context 物件來配置 bean 的資訊)知道了自己所需要呼叫的 controller 之後,他需要根據註解來提取其他的所需要的資訊。這時候就需要 HandlerAdapter 來做這些解析的事情。

然而,目前的 Spring MVC 的配置都基於註解,因此,HandlerAdapter 也退居幕後,@Controller 註解包含了其中邏輯,在 Annotation-driven 被我們關掉的場景下,也只要做好 HandlerMapping,就可以成功地對映你想要的 url

HandlerMapping

HandlerMapping 本質還是一個 Bean,他在 Spring MVC 裝配完成之後,執行著將 URL 的請求轉發到對應的 Controller 執行後續檢視,資料等返回的工作。因此,在配置 HandlerMapping Bean 的時候,需要配置 property 的 mappings 欄位,並且在 欄位下面指定對應的請求對映。具體程式碼如下:

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
     	<props>
         	<prop key="/handler-mapping.do">handlerMappingController</prop>
        </props>
	</property>
</bean>
複製程式碼

HandlerAdapter 和 HandlerMapping 的測試

為了同步一下,目前 root-context.xml (Spring Context 物件配置檔案) 的配置加入了 HandlerMapping 的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-3.2.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.test.myapp.example"/>

    <!--註冊一個用於 handlerMapping 的 bean 用於檢測 handlerMapping 效果-->
    <bean id="handlerMappingController" class="com.test.myapp.example.handlermapping.HandlerMappingController"/>

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/handler-mapping.do">handlerMappingController</prop>
            </props>
        </property>
    </bean>
    
    <!--<bean id="simpleHandler" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>-->
    <!--<mvc:annotation-driven/>-->
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:prefix="/WEB-INF/views/" p:suffix=".jsp" p:order="1">
    </bean>

</beans>
複製程式碼

並且新增了 HandlerMappingController.java 的配置:

package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Usage: 測試 handler mapping 的有效性
 * @author: srfan
 * Date: 10/26/18 4:11 PM
 */
@Controller
public class HandlerMappingController {

    @RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
    public String helloWorld() {
        return "handler_mapping_hello";
    }
}
複製程式碼

我們看到,HandlerMapping 下面配置了 /handler-mapping.do 的對映。因此,在執行工程之後,輸入 localhost:8080/handler-mapping.do,就可以看到對應的 handler_mapping_hello.jsp 上的前端檢視返回。

HandlerExceptionResolver 解析

HandlerExceptionResolver 是工程中用於捕獲特定 Exception 的 Bean,可以提前設定自己需要捕獲並且定向的 Exception,並且交由 HandlerExceptionResolver 對映到特定的檢視頁上面。 目前常用的方法有:

  • 實現 HandlerExceptionResolver 介面
  • 在方法上使用 @ExceptionHandler 註解

實現 HandlerExceptionResolver 介面

HandlerExceptionResolver 介面只有一個待實現的方法

ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4);
複製程式碼

為了工程上面比較直觀簡便的實現,我們只需要做最簡單的實現:拿到 Exception 的具體類,並且返回對應的 error 的檢視,並且記錄下 Exception 的 message,顯示在檢視頁面上面。因此我們的工序如下:

實現一個自定義的 Exception: MyCustomException

package com.test.myapp.example.handlermapping;

public class MyCustomException extends RuntimeException {
    public MyCustomException(String msg) {
        super(msg);
    }
}
複製程式碼

這個 Exception 類很簡單,只是把 message 放進 Exception 中,無需贅述,主要是要讓 ExceptionResolver 捕獲該 Exception。

實現 HandlerExceptionResolver 介面:ExceptionResolver

package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class ExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        if (e instanceof MyCustomException) {
            ModelAndView modelAndView = new ModelAndView("error");
            modelAndView.addObject("msg", e.getMessage());
            return modelAndView;
        }
        return null;
    }
}
複製程式碼

我們使用 ExceptionResolver 實現了 resolveException 方法,並且會解析 MyCustomException 並且在 ModelAndView 物件加入一個變數,並且返回名為 “error” 的 jsp 檢視。我們也可以在 error.jsp 上顯示這個 msg 欄位的資訊。

HandlerMappingController 新增兩個會丟擲 Exception 的介面

為了對照效果,我們實現兩個介面,一個會丟擲 MyCustomException,另一個則會丟擲普通的 IllegalArgumentException,而我們需要捕獲的則是 MyCustomException。

package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HandlerMappingController {

    @RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
    public String helloWorld() {
        return "handler_mapping_hello";
    }

    @RequestMapping(value="/custom-exception.do", method = RequestMethod.GET)
    public String throwException() {
        throw new MyCustomException("oh, you got custom exception message~!");
    }

    @RequestMapping(value="/argument-exception.do", method = RequestMethod.GET)
    public String throwArgumentException() {
        throw new IllegalArgumentException("oh, you got argument exception message~!");
    }
}
複製程式碼

檢視檔案 error.jsp 配置

檢視檔案 error.jsp 比較簡單,只要體現 msg 欄位即可:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Ooooops, you meet MyCustomException</title>
</head>
<body>
    <h1>${msg}</h1>
</body>
</html>
複製程式碼

測試

執行工程後,在瀏覽器分別輸入:

使用 @ExceptionHandler 註解

另一種方法是使用 @ExceptionHandler 的註解,該註解用於 method 的簽名上面,我們可以實現一個 Controller 的基類並讓實際接收 url 請求的 Controller 繼承該基類。值得注意的是,這個方法實現的 ExceptionResolver 只會在該 Controller 內部有效,而來自其他 Controller 類的 Exception 則無法得到解析。具體程式碼步驟如下:

設定自定義 Exception: CustomExceptionForAnnotation

我們為這一次測試也設定了自定義的 Exception 類,實現方法也很簡單,可以自定義 Exception 中的資訊:

package com.test.myapp.example.exceptionresolver;

public class CustomExceptionForAnnotation extends RuntimeException {
    public CustomExceptionForAnnotation(String msg) {
        super(msg);
    }
}
複製程式碼

實現有 @ExceptionHandler 註解的 Controller 基類

我們的 Controller 基類需要 Resolve CustomExceptionForAnnotation,需要用 @ExceptionHandler(CustomExceptionForAnnotation.class) 進行配置,具體方法如下:

package com.test.myapp.example.exceptionresolver;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

public abstract class BaseExceptionResolver {
    @ExceptionHandler({CustomExceptionForAnnotation.class})
    public ModelAndView handleCustomException(CustomExceptionForAnnotation ex) {
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("msg", ex.getMessage());
        return modelAndView;
    }
}
複製程式碼

可以看到,該類中所含有的方法僅會解析 CustomExceptionForAnnotation 類,並且將其重新導向 error.jsp 檢視,最後輸出對應的 message 資訊到前端。

實現兩個 Controller 類

為了使測試結果有對照性,我們實現了兩個 Controller 類,一個繼承自 BaseExceptionResolver,另一個則沒有。理論上說,繼承了 BaseExceptionResolver 的 Controller 將可以解析上面的 Exception,而另一個則不能。具體的配置方法如下:

  • 繼承了 BaseExceptionResolver 的 Controller 類
    package com.test.myapp.example.exceptionresolver;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class MyExceptionController extends BaseExceptionResolver {
    
        @RequestMapping("exception-for-annotation.do")
        public void exceptionForAnnotation() {
            throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message");
        }
    }
    複製程式碼
  • 未繼承 BaseExceptionResolver:
    package com.test.myapp.example.exceptionresolver;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class MyExceptionOutsideController {
    
        @RequestMapping("exception-for-annotation-outside.do")
        public void exceptionForAnnotation() {
            throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message");
        }
    }
    複製程式碼

測試

我們仍然使用了 error.jsp 檢視來做最後的測試工作,我們看到 BaseExceptionResolver 在捕獲異常後,仍然會輸出 error.jsp 的檢視。我們將會請求兩個具體 Controller 類的 url,觀察是否會有我們想要的檢視的輸出:

  • localhost:8080/exception-for-annotation.do: 成功輸出了我們放入 CustomExceptionForAnnotation 的資訊。
  • localhost:8080/exception-for-annotation-outside.do: 頁面輸出了 500 的錯誤資訊,並且帶上了 Exception 中的資訊,因為其沒有繼承 BaseExceptionResolver,因此也沒有對應的 Exception 解析器了。

小結

本章主要講述了 HandlerMapping 和 HandlerExceptionResolver 的具體實現程式碼,一個是處理正常的 url 請求的對映工具,而另一個則是專門處理工程在執行過程中出現 Exception 的處理方法。下一次我將繼續介紹後面這幾個特殊 Bean 的用法。

相關文章