1. 程式人生 > >springboot情操陶冶-web配置(五)

springboot情操陶冶-web配置(五)

本文講講mvc的異常處理機制,方便查閱以及編寫合理的異常響應方式

入口例子

很簡單,根據之前的文章,我們只需要複寫WebMvcConfigurer介面的異常新增方法即可,如下

1.建立簡單的異常處理類,本例針對繫結異常

package com.example.demo.web.validation;

import com.example.demo.web.model.ResEntity;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author nanco
 * -------------
 * resolve bindexception
 * -------------
 * @create 18/9/9
 */
public class SimpleExceptionResolver extends AbstractHandlerExceptionResolver {

    private static final Logger EXCEPTION_LOG = LoggerFactory.getLogger(SimpleExceptionResolver.class);

    private final Map<String, List<String>> errorResultMap = new HashMap<>(2);

    private final String ERROR_KEY = "error_result";

    private Gson gson = new Gson();

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // only process BindException,unless return null to allow the next handler understanding the exception
        if (BindException.class.isInstance(ex)) {
            ResEntity resEntity = new ResEntity();
            try {
                BindException bindException = BindException.class.cast(ex);
                List<ObjectError> allErrors = bindException.getAllErrors();

                List<String> resMessages = new ArrayList<>(allErrors.size());
                allErrors.stream().forEach(error -> {
                    resMessages.add(error.getDefaultMessage());
                });

                errorResultMap.put(ERROR_KEY, resMessages);

                resEntity.setData(errorResultMap);

                response.getOutputStream().write(gson.toJson(resEntity).getBytes());
            } catch (IOException e) {
                EXCEPTION_LOG.error("process BindException fail.", e);
            }

            return new ModelAndView();
        }
        return null;
    }
}

2.實現WebMvcConfigurer介面後複寫其中的extendHandlerExceptionResolvers()方法

package com.example.demo.web.config;

import com.example.demo.web.validation.SimpleExceptionResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * @author nanco
 * -------------
 * color the mvc config
 * -------------
 * @create 2018/9/5
 **/
@Configuration
public class BootWebMvcConfigurer implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

    }

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        // response first
        resolvers.add(0, new SimpleExceptionResolver());
    }
}

上述簡單的程式碼便會對系統丟擲的BindException異常進行鍼對性的處理,從而返回合乎格式的響應體。當然這只是一小部分,筆者可以稍微從原始碼的角度來分析下spring的異常機制

原始碼層

查閱過DispatcherServlet原始碼的都知道,當出現異常的時候,則會嘗試呼叫HandlerExceptionResolver解析器去根據異常進行檢視渲染或者直接返回對應的錯誤資訊。筆者按步驟來進行簡單分析,從WebMvcConfigurationSupport入手

1.異常解析器註冊

    @Bean
    public HandlerExceptionResolver handlerExceptionResolver() {
        List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
        // 優先載入使用者自定義的異常解析器,也可通過WebMvcConfigurer來複寫
        configureHandlerExceptionResolvers(exceptionResolvers);
        // 當用戶沒有複寫上述方法後,採取預設的異常解析器
        if (exceptionResolvers.isEmpty()) {
            addDefaultHandlerExceptionResolvers(exceptionResolvers);
        }
        // 擴增異常解析器,可見上文中的例子
        extendHandlerExceptionResolvers(exceptionResolvers);
        HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
        composite.setOrder(0);
        composite.setExceptionResolvers(exceptionResolvers);
        return composite;
    }

2.直接看下spring內建的預設異常解析器吧,參考addDefaultHandlerExceptionResolvers()方法

    protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        // 1.異常的方法處理,跟@RequestMapping註解的方法呼叫類似
        ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
        exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager());
        exceptionHandlerResolver.setMessageConverters(getMessageConverters());
        exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
        exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
        if (jackson2Present) {
            exceptionHandlerResolver.setResponseBodyAdvice(
                    Collections.singletonList(new JsonViewResponseBodyAdvice()));
        }
        if (this.applicationContext != null) {
            exceptionHandlerResolver.setApplicationContext(this.applicationContext);
        }
        exceptionHandlerResolver.afterPropertiesSet();
        exceptionResolvers.add(exceptionHandlerResolver);
        // 2.攜帶@ResponseStatus註解的解析器
        ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
        responseStatusResolver.setMessageSource(this.applicationContext);
        exceptionResolvers.add(responseStatusResolver);
        // 3.預設的異常解析器,針對spring的內建異常作下簡單的response
        exceptionResolvers.add(new DefaultHandlerExceptionResolver());
    }

筆者主要關注ExceptionHandlerExceptionResolverResponseStatusExceptionResolver解析器,那就分塊來簡單的講解把

ExceptionHandlerExceptionResolver

初始化狀態的程式碼就不羅列了,讀者直接閱讀原始碼就知道,筆者此處作下初始化的總結

  1. 尋找所有的攜帶@ControllerAdvice註解的bean,包裝成ExceptionHandlerMethodResolver方法解析器,由此來從中挑選出攜帶@ExceptionHandler註解的方法集合

  2. 對第一條中所得的方法集合,讀取其中@ExceptionHandler註解的值(Throwable實現類);無則讀取對應方法實現了Throwable異常介面的引數集合。即得出exceptionTypes集合

  3. 對上述的exceptionTypes集合依次與對應的method形成對映,即方便針對指定的異常可以呼叫相應的方法來返回結果

  4. 對上述滿足條件的ControllerAdvice ,結合ExceptionHandlerMethodResolver裝入exceptionHandlerAdviceCache屬性map中

  5. 封裝引數解析器集合與返回值解析器集合,和處理@RequestMapping的操作一樣

具體的解析過程,筆者此處點一下,方便與上文對照著看,直接看關鍵的getExceptionHandlerMethod()方法

    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
            @Nullable HandlerMethod handlerMethod, Exception exception) {

        Class<?> handlerType = null;

        if (handlerMethod != null) {
            // 獲取出現異常類方法的所在類
            handlerType = handlerMethod.getBeanType();
            // 優先判斷如果此類直接返回的是異常類,則嘗試尋找解析器
            ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
            if (resolver == null) {
                resolver = new ExceptionHandlerMethodResolver(handlerType);
                this.exceptionHandlerCache.put(handlerType, resolver);
            }
            // 得到對映的方法
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
            }
            // For advice applicability check below (involving base packages, assignable types
            // and annotation presence), use target class instead of interface-based proxy.
            if (Proxy.isProxyClass(handlerType)) {
                handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
            }
        }
        // 進入@ControlleAdvice的語法環境了,判斷拋異常的所在類,ControllerAdvice是否支援
        for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
            ControllerAdviceBean advice = entry.getKey();
            if (advice.isApplicableToBeanType(handlerType)) {
                ExceptionHandlerMethodResolver resolver = entry.getValue();
                Method method = resolver.resolveMethod(exception);
                if (method != null) {
                    return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
                }
            }
        }

        return null;
    }

最終就是根據Exception的型別找尋符合條件的method,然後按照@RequestMapping註解的處理方式得到相應的檢視物件供檢視解析器去渲染

ResponseStatusExceptionResolver

針對攜帶@ResponseStatus註解的異常類來返回響應體的,簡單的看下程式碼吧

    @Override
    @Nullable
    protected ModelAndView doResolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

        try {
            // 直接返回的是ResponseStatusException型別的異常則直接處理
            if (ex instanceof ResponseStatusException) {
                return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
            }
            // 讀取異常類上攜帶的@ResponseStatus註解,有則返回結果
            ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
            if (status != null) {
                return resolveResponseStatus(status, request, response, handler, ex);
            }
            // 遞迴呼叫下
            if (ex.getCause() instanceof Exception) {
                ex = (Exception) ex.getCause();
                return doResolveException(request, response, handler, ex);
            }
        }
        catch (Exception resolveEx) {
            logger.warn("ResponseStatus handling resulted in exception", resolveEx);
        }
        // 無符合條件的,直接返回null,呼叫下一個異常解析器
        return null;
    }

最終呼叫的也就是HttpServletResponse#sendError(int statusCode,String reason)方法直接返回結果

DispatcherServlet異常處理邏輯

此處還是貼下重要的程式碼片段,加深印象,直接查閱processHandlerException()方法

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

....
if (this.handlerExceptionResolvers != null) {
            // 對異常解析器集合進行遍歷
            for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
                exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
                // ModelAndView物件不為null則直接跳出,否則採取下一個異常解析器
                if (exMv != null) {
                    break;
                }
            }
        }

}
....

溫馨提示:

  1. 根據上述程式碼的邏輯可見,使用者在自定義相應的異常解析器時,需要注意如果滿足解析指定的異常,則最後返回不為null的檢視物件(return new ModelAndView()),以免其跑至下一個異常解析器,影響服務執行結果。
  2. 遍歷的異常解析器順序此處提一下,其採取的是簡單的ArrayList集合來保持順序,所以使用者如果想自己的異常解析器保持較高的優先順序,則可以採取List介面的add(int index, T value)方法新增或者直接實現HandlerExceptionResolver並設定order屬性來保持即可

結語

瞭解異常解析器的載入機制以及執行邏輯,方便我們寫出合乎spring邏輯的程式碼,以此保證程式碼的整潔性。