1. 程式人生 > 實用技巧 >[Re] SpringMVC-5

[Re] SpringMVC-5

HttpMessageConverter

HMC 簡述

  • HttpMessageConverter<T> 是 Spring3.0 新新增的一個介面,負責將請求資訊轉換為一個物件(型別為 T),將物件(型別為 T)輸出為響應資訊
  • 介面定義的方法
    Boolean canRead(Class<?> clazz, MediaType mediaType)
        指定轉換器可以讀取的物件型別,即轉換器是否可將請求資訊轉換為 clazz 類
        型的物件,同時指定支援 MIME 型別(text/html,applaiction/json等)。
    Boolean canWrite(Class<?> clazz, MediaType mediaType)
        指定轉換器是否可將 clazz 型別的物件寫到響應流中,響應流支援的媒體型別
        在 MediaType 中定義。
    List<MediaType> getSupportMediaTypes()
        該轉換器支援的媒體型別。
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
        將請求資訊流轉換為 T 型別的物件 // 結合上圖
    void write(T t, MediaType contnetType, HttpOutputMessgae outputMessage)
        將 T 型別的物件寫到響應流中,同時指定相應的媒體型別為 contentType // 結合上圖
    

HMC 實現類

  • 使用 HttpMessageConverter<T> 將請求資訊轉化並繫結到處理方法的形參中或將響應結果轉為對應型別的響應資訊,Spring 提供了兩種途徑:
    • 使用 @RequestBody / @ResponseBody 對處理方法進行標註
    • 使用 HttpEntity<T> / ResponseEntity<T> 作為處理方法的形參或返回值
  • 當控制器處理方法使用到 @RequestBody/@ResponseBodyHttpEntity<T>/ResponseEntity<T> 時,Spring 首先根據請求頭或響應頭的 Accept 屬性選擇匹配的 HttpMessageConverter, 進而根據引數型別或泛型型別的過濾得到匹配的 HttpMessageConverter,若找不到可用的 HttpMessageConverter 將報錯

響應 Ajax

  1. 加入 jar 包
  2. 編寫目標方法,使其返回/接收 JSON 對應的物件或集合
  3. 在方法上新增 @ResponseBody | @RequestBody 註解

@ResponseBody

  • @ResponseBody 將目標方法返回的資料放入響應體
    • 如果是物件,匯入的 Jackson.jar 會自動將物件轉為 json 格式
    • 如果是檢視名,就在客戶端瀏覽器列印檢視名,不會再轉發到對應檢視了
  • json 還有很多可以加在 JavaBean 屬性上的註解,如 :
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date birth = new Date();
    

@RequestBody

  • @RequestBody 可以獲取一個(POST)請求的請求體
  • 既然 @ResponseBody 可以把物件轉為 json 返回給瀏覽器;那麼 @RequestBody 自然也就可以反向操作,接收 json 資料再封裝成物件。
    @RequestMapping("/testRequestBody")
    public String testRequestBody(@RequestBody Employee reqBodyToEmp) {
        System.out.println(reqBodyToEmp);
        return "../../ajax";
    }
    ·····························································
    $("#btn2").click(function() {
        // 傳送 Ajax 請求
        var emp = {lastName:'張三', email:'[email protected]', gender:1};
        var empStr = JSON.stringify(emp);
        // alert(typeof emp); // Object
        // alert(typeof empStr); // String
        $.ajax({
            url: '${pageContext.request.contextPath }/testRequestBody',
            type: "POST",
            data: empStr,
            contentType: "application/json",
            success:function(data) {
                alert(data);
            }
        });
        return false;
    });
    
  • 請求頭一覽

HttpEntity

可以拿到所有請求頭。

@RequestMapping("/test01")
public String test01(HttpEntity<String> str) {
    System.out.println(str);
    return "../../ajax";
}
·····························································
<,{accept=[image/gif, image/jpeg, image/pjpeg, application/x-ms-application
, application/xaml+xml, application/x-ms-xbap, */*], accept-language=[zh-Hans-CN
,zh-Hans;q=0.5], ua-cpu=[AMD64], accept-encoding=[gzip, deflate], user-agent
=[Mozilla/5.0 (Windows NT 6.2; Win64; x64; Trident/7.0; rv:11.0) like Gecko]
,host=[localhost:8080], connection=[Keep-Alive],cookie=[JSESSIONID
=917AF2A4399F13061B76B71299B7D7C1; Webstorm-d70cb02f
=56d06134-d193-4b9a-9add-38fde8c9e207]}>

ResponseEntity(檔案下載)

@RequestMapping("/test02")
public ResponseEntity<String> test02() {
    MultiValueMap<String, String> headers = new HttpHeaders();
    String body = "<h1>SUCCESS</h1>";
    headers.add("Set-Cookie", "root=shaw");
    ResponseEntity<String> responseEntity = new
            ResponseEntity<String>(body, headers, HttpStatus.OK);
    return responseEntity;
}


@RequestMapping("/download")
public ResponseEntity<byte[]> download(HttpServletRequest request) throws Exception{
    // 1. 找到要下載的檔案的真實路徑
    ServletContext context = request.getServletContext();
    String realPath = context.getRealPath("/scripts/jquery-1.9.1.min.js");
    // 2. 得到要下載的檔案的流;
    FileInputStream is = new FileInputStream(realPath);
    byte[] tmp = new byte[is.available()];
    is.read(tmp);
    is.close();
    // 3. 將要下載的檔案流返回
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set("Content-Disposition", "attachment;filename="+"jquery-1.9.1.min.js");
    return new ResponseEntity<byte[]>(tmp, httpHeaders, HttpStatus.OK);
}

檔案上傳

  • Spring MVC 為檔案上傳提供了直接的支援,這種支援是通過即插即用的 MultipartResolver(九大元件之一) 實現的。

  • Spring 用 Jakarta Commons FileUpload 技術實現了一個 MultipartResolver 實現類:CommonsMultipartResovler。但 Spring MVC 上下文中預設沒有裝配 MultipartResovler,因此預設情況下不能處理檔案的上傳工作,如果想使用 Spring 的檔案上傳功能,需現在上下文中配置 MultipartResolver。

    <bean id="multipartResolver" 
            class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 最大檔案上傳大小 -->
        <property name="maxUploadSize" value="#{1024*1024*20}"></property>
        <!-- 設定預設的字元編碼,必須和 JSP 的 pageEncoding 屬性一致 -->
        <property name="defaultEncoding" value="utf-8"></property>
    </bean>
    
  • 為了讓 CommonsMultipartResovler 正確工作,必須先將 Jakarta Commons FileUpload 及 Jakarta Commons io 的類包新增到類路徑下。

  • 檔案上傳示例

    <form action="${pageContext.request.contextPath }/upload"
            method="post" enctype="multipart/form-data">
        使用者頭像:<input type="file" name="headerImg" /><br/>
        使用者名稱:<input type="text" name="username" /><br/>
        <input type="submit" value="提交"/>
    </form>
    
    @Controller
    public class FileUploadController {
        <!--
         @RequestParam("headerImg") MultipartFile file
         設定檔案上傳項在目標方法的形參為 ↑
         如果是多檔案上傳,將形參型別變成對應型別的陣列即可
         -->
        @RequestMapping("/upload")
        public String upload(@RequestParam("username") String username
                , @RequestParam("headerImg") MultipartFile file) {
            System.out.println("檔案項name:" + file.getName());
            System.out.println("原檔名: " + file.getOriginalFilename());
    
            // 檔案儲存
            try {
                file.transferTo(new File("U:\\" + file.getOriginalFilename()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "forward:/upload.jsp";
        }
    }
    

攔截器

HandlerInterceptor

Spring MVC 也可以使用攔截器對請求進行攔截處理,使用者可以自定義攔截器來實現特定的功能,自定義的攔截器必須實現 HandlerInterceptor 介面。

  • preHandle() 在業務處理器處理請求之前被呼叫,在該方法中對使用者請求 request 進行處理。如果程式設計師決定該攔截器對請求進行攔截處理後還要呼叫其他的攔截器,或者是業務處理器去進行處理,則返回 true;如果程式設計師決定不需要再呼叫其他的元件去處理請求,則返回 false。
  • postHandle() 在業務處理器處理完請求後,但是 DispatcherServlet 向客戶端返回響應前被呼叫,在該方法中對使用者請求 request 進行處理。
  • afterCompletion() 在 DispatcherServlet 完全處理完請求(資源已響應) 後被呼叫,可以在該方法中進行一些資源清理的操作。

攔截器執行流程

單攔截器執行流程

  • Controller
    @Controller
    public class TestInterceptorController {
    
        @RequestMapping("/test01")
        public String test01() {
            System.out.println("Method test01");
            return "success";
        }
    }
    
  • index.jsp
    <a href="${pageContext.request.contextPath }/test01">test01</a>
    
  • success.jsp
    <body>
    <%System.out.println("SUCCESS JSP"); %>
    </body>
    
  • FirstInterceptor(SecondInterceptor 同理)
    public class FirstInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request
                , HttpServletResponse response, Object handler) throws Exception {
            System.out.println("[FirstInterceptor] preHandle");
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response
                , Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("[FirstInterceptor] postHandle");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse
                response, Object handler, Exception ex) throws Exception {
            System.out.println("[FirstInterceptor] afterCompletion");
        }
    }
    
  • springMVC.xml
    <!-- 配置攔截器(誰靠前,誰優先) -->
    <mvc:interceptors>
        <!-- 配置第一個攔截器,攔截所有請求 -->
        <bean class="cn.edu.nuist.component.FirstInterceptor"></bean>
        <!-- 配置第二個攔截器更詳細的資訊,攔截指定資源 -->
        <mvc:interceptor>
            <!-- 只攔截 test01 請求 -->
            <mvc:mapping path="/test01"/>
            <bean class="cn.edu.nuist.component.SecondInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
    

  • 正常執行流程:preHandle → 目標方法 → postHandle → 響應資源 → afterCompletion
  • 其他流程
    • preHandle 返回 false
    • 目標方法拋異常

多攔截器執行流程

  • 正常流程
  • Second 不放行(已放行攔截器的 afterCompletion 總會執行)

檢視原始碼

DispatcherServlet

doDispatcher() 部分原始碼:

try {
    // ...
    try {
        // ...

        // 【攔截器 preHandle 執行位置】只要有一個返false,直接跳到 afterCompletion
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
        }

        try {
            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
        }

        applyDefaultViewName(request, mv);

        // 【攔截器 postHandle 執行位置】逆序執行每一個攔截器的 postHandle
        mappedHandler.applyPostHandle(processedRequest, response, mv);
    }
    catch (Exception ex) {
        dispatchException = ex;
    }

    // 【頁面渲染】如果拋異常,直接被下面catch住,執行 afterCompletion
    processDispatchResult(processedRequest
        , response, mappedHandler, mv, dispatchException);

}
catch (Exception ex) {
    // 【最外圍 catch】抓住異常,依舊正常執行 afterCompletion
    triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
    triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
    if (asyncManager.isConcurrentHandlingStarted()) {
        // Instead of postHandle and afterCompletion
        mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
        return;
    }
    // Clean up any resources used by a multipart request.
    if (multipartRequestParsed) {
        cleanupMultipart(processedRequest);
    }
}

processDispatchResult() 部分原始碼:

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
    render(mv, request, response);
    if (errorView) {
        WebUtils.clearErrorRequestAttributes(request);
    }
}

// ...

// 頁面渲染完成後,順序執行到此處,執行 afterCompletion
// 即使渲染出錯,沒走到這,在外層 catch 塊裡還是會順利執行
if (mappedHandler != null) {
    mappedHandler.triggerAfterCompletion(request, response, null);
}

preHandle

boolean applyPreHandle(HttpServletRequest request
        , HttpServletResponse response) throws Exception {
    if (getInterceptors() != null) {
        for (int i = 0; i < getInterceptors().length; i++) {
            HandlerInterceptor interceptor = getInterceptors()[i];
            // 若返回 true,繼續迴圈遍歷下一個 Interceptor
            // 若返回 false,除退出該方法外,觸發呼叫放行攔截器的 afterCompletion
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, null);
                return false;
            }
            // 記錄能走到這兒(放行)的攔截器的最大索引
            this.interceptorIndex = i;
        }
    }
    return true;
}

postHandle

void applyPostHandle(HttpServletRequest request, HttpServletResponse
         response, ModelAndView mv) throws Exception {
    if (getInterceptors() == null) {
        return;
    }

    for (int i = getInterceptors().length - 1; i >= 0; i--) {
        HandlerInterceptor interceptor = getInterceptors()[i];
        interceptor.postHandle(request, response, this.handler, mv);
    }
}

afterCompletion

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse
        response, Exception ex) throws Exception {

    if (getInterceptors() == null) {
        return;
    }

    // i: 放行攔截器的索引
    for (int i = this.interceptorIndex; i >= 0; i--) {
        HandlerInterceptor interceptor = getInterceptors()[i];
        try {
            interceptor.afterCompletion(request, response, this.handler, ex);
        }
        catch (Throwable ex2) {
            logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
        }
    }
}

Filter 和 Interceptor

  • 如果某些功能,需要其他元件配合完成,就是用 Interceptor;其他情況可以寫 Filter
  • Interceptor 脫離 SpringMVC 就沒用了,而 Filter 是 JavaWeb 三大元件

國際化

簡單使用

  1. 寫好國際化資原始檔
  2. 讓 Spring 的 ResourceBundleMessageSource 管理國際化資原始檔
    <bean id="messageSource" 
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="/loginPage/login"></property>
    </bean>
    
  3. 直接去頁面取值
    <h1><fmt:message key="welcomeInfo" /></h1>
    <form action="#">
        <fmt:message key="username" /><input type="text" name="username"/><br/>
        <fmt:message key="password" /><input type="password" name="password"/><br/>
        <input type="submit" value="<fmt:message key="loginBtn" />">
    </form>
    

AcceptHeaderLocaleResolver

  • 國際化的區域資訊是決定國際化顯示的因素。而 SpringMVC 中的區域資訊是由區域資訊解析器得到的:private LocaleResolver localeResolver;
  • 檢視 DispatcherServlet.properties 可知:預設使用 AcceptHeaderLocaleResolver
    public class AcceptHeaderLocaleResolver implements LocaleResolver {
    
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            return request.getLocale(); // 使用請求頭的區域資訊
        }
    
        @Override
        public void setLocale(HttpServletRequest request
                , HttpServletResponse response, Locale locale) {
            throw new UnsupportedOperationException("Cannot change HTTP accept header"
                    + " - use a different locale resolution strategy");
        }
    }
    
  • 凡是用到區域資訊的地方都是呼叫上述解析器的方法得到的;以渲染檢視為例:
    protected void render(ModelAndView mv, HttpServletRequest request
            , HttpServletResponse response) throws Exception {
        // Determine locale for request and apply it to the response.
        Locale locale = this.localeResolver.resolveLocale(request);
        response.setLocale(locale);
    
        // ...
    }
    

程式中獲取國際化資訊

@Controller
public class TestI18nController {

    @Autowired // 程式中獲取國際化資訊
    private MessageSource messageSource;

    @RequestMapping("toLoginPage")
    public String toLoginPage(Locale locale) {
        System.out.println(locale); // zh_CN
        String welcomeInfo = messageSource.getMessage("welcomeInfo", null, locale);
        System.out.println(welcomeInfo);
        return "login";
    }
}

點選超連結切換國際化

<a href="toLoginPage?locale=zh_CN">中文</a>|
<a href="toLoginPage?locale=en_US">English</a>

自定義區域資訊解析器

public class MyLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String localeStr = request.getParameter("locale");
        Locale l = null; // zh_CN
        if(localeStr != null && !"".equals(localeStr)) {
            l = new Locale(localeStr.split("_")[0], localeStr.split("_")[1]);
        } else {
            l = request.getLocale();
        }
        return l;
    }

    @Override
    public void setLocale(HttpServletRequest request
            , HttpServletResponse response, Locale locale) {
        throw new UnsupportedOperationException(
                "Cannot change HTTP accept header "
                + "- use a different locale resolution strategy");
    }
}

SessionLocaleResolver

  • SessionLocaleResolver 的區域資訊是從 session 中獲取;也可以根據請求引數建立一個 Locale 物件,把他放在 session 中
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale locale = (Locale) WebUtils.getSessionAttribute(
                request, LOCALE_SESSION_ATTRIBUTE_NAME);
        if (locale == null) {
            locale = determineDefaultLocale(request);
        }
        return locale;
    }
    
  • LocaleChangeInterceptor 部分原始碼
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response
            , Object handler) throws ServletException {
    
        String newLocale = request.getParameter(this.paramName);
        if (newLocale != null) {
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException("No LocaleResolver found:"
                    + "not in a DispatcherServlet request?");
            }
            localeResolver.setLocale(request, response
                    , StringUtils.parseLocaleString(newLocale));
        }
        // Proceed in any case.
        return true;
    }
    
  • 配置到 springMVC.xml
  • 工作原理

異常處理

HandlerExceptionResolver

  • Spring MVC 通過 HandlerExceptionResolver 處理程式的異常,包括 Handler 對映、資料繫結以及目標方法執行時發生的異常。
  • 檢視 DispatcherServlet.properties 檔案:
    HandlerExceptionResolver =
        AnnotationMethodHandlerExceptionResolver(被ExceptionHandlerExceptionResolver取代),
        ResponseStatusExceptionResolver,
        DefaultHandlerExceptionResolver
    
  • 若使用了 <mvc:annotation-driven/> ,檢視 AnnotationDrivenBeanDefinitionParser.parse() 方法:
    RootBeanDefinition exceptionHandlerExceptionResolver =
            new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
    

三個預設異常解析器的使用

這 3 個異常解析器各自能處理什麼樣的異常?

ExceptionHandlerExceptionResolver

主要處理 Handler 中用 @ExceptionHandler 註解定義的方法

  • @ExceptionHandler 註解定義的方法優先順序問題:例如發生的是 NullPointerException,但是宣告的異常有 RuntimeException 和 Exception,此候會根據異常的最近繼承關係找到繼承深度最淺的那個 @ExceptionHandler 註解方法,即標記了 RuntimeException 的方法
  • ExceptionHandlerMethodResolver 內部若找不到 @ExceptionHandler 註解的話,會找 @ControllerAdvice 標註的類(全域性異常處理類) 中的 @ExceptionHandler 註解方法,看有沒有哪個方法能處理該異常
  • Tips
    • @ExceptionHandler 註解定義的方法可新增一個 Exception 型別的方法形參,用來接收異常物件
    • 如何攜帶異常資訊到頁面 → 目標方法值型別為 ModelAndView(不能在形參上寫 Model/Map,這個不是目標方法;而是個異常處理方法,只認異常型別引數)
    • 寫在 Controller 裡,只對當前 Controller 有效;寫在 @ControllerAdvice 標註的類裡,全域性有效;對某異常的處理若同時存在,本類優先,本類能處理的,自己處理;處理不了的,再交給全域性。
  • 示例
@ExceptionHandler(value= {ArithmeticException.class, NullPointerException.class})
public ModelAndView handleException01(Exception e) {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("myError");
    mav.addObject("ex", e);
    return mav;
}

ResponseStatusExceptionResolver

當目標處理方法丟擲 {被 @ResponseStatus 註解的} 異常

  • 定義一個 @ResponseStatus 註解修飾的異常類
    @ResponseStatus(reason = "使用者被拒絕登入", value=HttpStatus.NOT_ACCEPTABLE)
    public class UserNameNotFoundException extends RuntimeException {}
    
  • 若在處理器方法中丟擲了上述異常:若 ExceptionHandlerExceptionResolver 不解析上述異常,而又由於觸發的異常 UserNameNotFoundException 帶有 @ResponseStatus 註解。因此會被 ResponseStatusExceptionResolver 解析到,然後使用該註解的屬性進行處理:① 響應 value 屬性對應的 HttpStatus 程式碼給客戶端 ② 錯誤資訊為 reason 屬性值。

DefaultHandlerExceptionResolver

對 Spring 自定義異常進行處理。

NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException、
HttpMediaTypeNotSupportedException、HttpMediaTypeNotAcceptableException ...

doResolveException() 原始碼:

if (ex instanceof NoSuchRequestHandlingMethodException) {
    return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex
            , request, response, handler);
}
else if (ex instanceof HttpRequestMethodNotSupportedException) {
    return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex
            , request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
    return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex
            , request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
    return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex
            , request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
    return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex
            , request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
    return handleServletRequestBindingException((ServletRequestBindingException) ex
            , request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
    return handleConversionNotSupported((ConversionNotSupportedException) ex
            , request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
    return handleTypeMismatch((TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
    return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex
            , request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
    return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex
            , request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
    return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex
            , request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
    return handleMissingServletRequestPartException((MissingServletRequestPartException) ex
            , request, response, handler);
}
else if (ex instanceof BindException) {
    return handleBindException((BindException) ex
            , request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
    return handleNoHandlerFoundException((NoHandlerFoundException) ex
            , request, response, handler);
}

異常處理流程

DispatcherServlet

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response
        , HandlerExecutionChain mappedHandler, ModelAndView mv
        , Exception exception) throws Exception {

    boolean errorView = false;

    // 如果有異常
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else { // 處理異常!!!
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isDebugEnabled()) {
            logger.debug("Null ModelAndView returned to DispatcherServlet with name '"
                    + getServletName() + "': assuming HandlerAdapter"
                        + "completed request handling");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

protected ModelAndView processHandlerException(HttpServletRequest request
        , HttpServletResponse response, Object handler, Exception ex) throws Exception {

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
        exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
            break;
        }
    }

    if (exMv != null) {
        if (exMv.isEmpty()) {
            return null;
        }
        // We might still need view name translation for a plain error model...
        if (!exMv.hasView()) {
            exMv.setViewName(getDefaultViewName(request));
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Handler execution resulted in exception "
                 + "- forwarding to resolved error view: " + exMv, ex);
        }
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }

    // 如果異常解析器都不能處理,就直接丟擲去
    throw ex;
}

SimpleMappingExceptionResolver

通過配置的方式進行異常處理。

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <!-- 配置哪些異常去哪些頁面 -->
    <property name="exceptionMappings">
        <props>
        <!-- key: 異常全類名;value: 要去的頁面檢視名 -->
        <prop key="java.lang.ArrayIndexOutOfBoundsException">myError</prop>
        </props>
    </property>
    <!-- 指定錯誤資訊取出時,用的 key 的名字(預設叫 exception) -->
    <property name="exceptionAttribute" value="ex"></property>
</bean>