【原創】遨遊springmvc之HandlerExceptionResolver

1.前言

在咱們的程序中,不少時候會碰到對異常的處理,咱們也許會定義一些本身特殊業務的異常,在發生錯誤的時候會拋出異常,在springmvc的實際應用中,咱們常常須要返回異常的信息以及錯誤代碼,而且對異常進行一些處理而後返回再返回視圖。這就要涉及到咱們這一篇主要講的HandlerExceptionResolver前端

2.原理

其實springmvc已經默認給咱們注入了3個異常處理的解器:java

AnnotationMethodHandlerExceptionResolver(針對@ExceptionHandler,3.2已廢除,轉而使用ExceptionHandlerExceptionResolver)
ResponseStatusExceptionResolver(針對加了@ResponseStatus的exception)
DefaultHandlerExceptionResolver(默認異常處理器)git

2.1 依賴

2.1.1 解析器依賴

圖小能夠放大!😳github

 

2.1.2 springmvc內部處理的一些標準異常

2.2 接口說明

public interface HandlerExceptionResolver {

	/**
	 * Try to resolve the given exception that got thrown during handler execution,
	 * returning a {@link ModelAndView} that represents a specific error page if appropriate.
	 * <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
	 * to indicate that the exception has been resolved successfully but that no view
	 * should be rendered, for instance by setting a status code.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler the executed handler, or {@code null} if none chosen at the
	 * time of the exception (for example, if multipart resolution failed)
	 * @param ex the exception that got thrown during handler execution
	 * @return a corresponding {@code ModelAndView} to forward to, or {@code null}
	 * for default processing
	 */
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

}

HandlerExceptionResolver只有一個核心方法,就是resolveException,方法體中包含處理的方法,異常已經請求和響應參數。web

在咱們本身去實現自定義異常解析器的時候,咱們通常是去繼承AbstractHandlerExceptionResolverspring

AbstractHandlerExceptionResolver實現了HandlerExceptionResolver和Orderedjson

那麼針對異常的處理具體是在哪裏執行的呢?mvc

答案是springmvc核心類DispatcherServletapp

在DispatcherServlet的doDispatch()方法最後會執行異步

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

它將異常給統一處理了!

咱們先來看下DispatcherServlet類中的兩個方法:

源碼2.2.1

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()) {
				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
				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;
	}

在以上源碼可知:

1)異常處理器只有當返回的ModelAndView不是空的時候纔會返回最終的異常視圖,當異常處理返回的ModelAndView若是是空,那麼它將繼續去下一個異常解析器。

2)異常解析器是有執行順序的,咱們在合適的場景能夠定義本身的order來絕對哪一個異常解析器先執行,order越小,越先執行

源碼2.2.2

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);
		}
	}

當異常返回的視圖ModelAndView不是空的時候,DispatcherServlet最終會重定向到執行View。

3.實例

咱們接下來要實現2種自定義異常處理器

  1. 實現rest下的異常處理返回json信息,附加validate驗證
  2. 自定義頁面異常
  3. 經過ControllerAdvice

先上一個rest的response的一個標準實體

/**
 * <p class="detail">
 * 功能:REST接口標準容器
 * </p>
 * @param <T> the type parameter
 *
 * @author Kings
 * @ClassName Rest response.
 * @Version V1.0.
 * @date 2016.08.16 09:28:55
 */
@Setter
@Getter
public class RestResponse<T> {
    /**
     * The constant VOID_REST_RESPONSE.
     */
    public static final RestResponse<Void> VOID_REST_RESPONSE = new RestResponse<>(null);
    
    @ApiModelProperty(value = "狀態碼", required = true)
    private int code;
    
    @ApiModelProperty(value = "服務端消息", required = true)
    private String message;
    
    @ApiModelProperty (value = "數據")
    private T data = null;
    
    /**
     * Instantiates a new Rest response.
     * @param code    the code
     * @param message the message
     * @param data    the data
     */
    public RestResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        if (data != null && "class com.github.pagehelper.PageInfo".equals(data.getClass().toString())) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("pageInfo", data);
            this.data = (T) map;
        } else {
            this.data = data;
        }
        
    }
    
    /**
     * Instantiates a new Rest response.
     * @param status the status
     */
    public RestResponse(HttpStatus status, T data) {
        this(status.value(), status.getReasonPhrase(), data);
    }
    
    /**
     * Instantiates a new Rest response.
     * @param data the data
     */
    public RestResponse(T data) {
        this(HttpStatus.OK.value(), "OK", data);
    }
    
    
    
    @Override
    public String toString() {
        return "{\"code\":" + code + ",\"message\":\"" + message + "\",\"data\":" + data + "}";
    }
}

3.1 Rest異常解析器

先上springmvc validate切面實現錯誤信息綁定,validate是經過切面來實現,省去控制器層一大堆對BindingResult處理代碼。

3.1.1 ErrorMessage

public class ErrorMessage {
    
    /** 字段名 */
    private String fieldName;
    /** 錯誤提示. */
    private String message;
    
    /**
     * Instantiates a new Error message.
     * @param fieldName the field name
     * @param message   the message
     */
    public ErrorMessage(String fieldName, String message) {
        this.fieldName = fieldName;
        this.message = message;
    }
    
    /**
     * Gets field name.
     * @return the field name
     */
    public String getFieldName() {
        return fieldName;
    }
    
    /**
     * Gets message.
     * @return the message
     */
    public String getMessage() {
        return message;
    }
    
    @Override
    public String toString() {
        return "{\"fieldName\":\""+fieldName+"\",\"message\":\""+message+"\"}";
    }
}

validate錯誤信息實體

 

3.1.2 @ValidMethod

/**
 * <p class="detail">
 * 功能:驗證註解,aop掃描次註解進行錯誤信息的綁定輸出
 * </p>
 * @author Kings
 * @ClassName Validate method.
 * @Version V1.0.
 * @date 2016.08.03 17:51:01
 */
@Retention (RetentionPolicy.RUNTIME)
@Target (ElementType.METHOD)
public @interface ValidateMethod {
    
}

 

3.1.3 ValidateException

/**
 * <p class="detail">
 * 功能:驗證異常
 * </p>
 * @author Kings
 * @ClassName Validate exception.
 * @Version V1.0.
 * @date 2016.08.09 14:45:58
 */
@ResponseStatus (value = HttpStatus.BAD_REQUEST, code = HttpStatus.BAD_REQUEST)
public class ValidateException extends RuntimeException {
    
    /**
     * Instantiates a new Validate exception.
     * @param message the message
     */
    public ValidateException(String message) {
        super(message);
    }
}

狀態碼定義是400

 

3.1.4 ErrorHelper

public class ErrorHelper {
    private static Logger logger = LoggerFactory.getLogger(ErrorHelper.class);
    
    public RestResponse converBindError2AjaxError(BindingResult result, boolean validAllPropeerty) {
        
        try {
            RestResponse res = new RestResponse(HttpStatus.BAD_REQUEST,"validate error!");
    
            List<ErrorMessage> errorMesages = new ArrayList<>();
            List<ObjectError> objectErrors = result.getAllErrors();
            for (ObjectError objError : objectErrors) {
                if (objError instanceof FieldError) {
                    FieldError objectError = (FieldError) objError;
                    errorMesages.add(new ErrorMessage(objectError.getField(), objError.getDefaultMessage()));
                } else {
                    errorMesages.add(new ErrorMessage(objError.getCode(), objError.getDefaultMessage()));
                }
                if(!validAllPropeerty){
                    //noinspection BreakStatement
                    break;//just one error object    
                }
            }
            res.setData(errorMesages);
            return res;
        } catch (Exception e) {
            logger.error("com.gttown.common.support.web.validate.ErrorHelper error",e);
        }
        return null;
    }
}

 

3.1.5 ValidHandlerAspect

/**
 * <p class="detail">
 * 功能:驗證切面
 * </p>
 * @author Kings
 * @ClassName Validate handel aspect.
 * @Version V1.0.
 * @date 2016.08.03 17:51:42
 */
@Aspect
public class ValidateHandelAspect {
    /**judge is all property error need to be export*/
    private boolean outputAllPropError = false;
    
    /**
     * <p class="detail">
     * 功能:驗證輸出結果
     * </p>
     * @param pjp :
     *
     * @return object
     * @throws Throwable the throwable
     * @author Kings
     * @date 2016.08.03 17:51:42
     */
    @Around ("validatePointcut()")
    public Object validateAround(ProceedingJoinPoint pjp) throws Throwable  {
        Object[] args =  pjp.getArgs();
        BindingResult bindingResult = null;
        if (args != null) {
            for (Object obj : args) {
                if (obj instanceof BindingResult) {
                    bindingResult = (BindingResult) obj;
                    //noinspection BreakStatement
                    break;
                }
            }
        }
        
        if ( bindingResult != null && bindingResult.hasErrors() ){//異常輸出
            ErrorHelper errorHelper = new ErrorHelper(); 
            throw new ValidateException(errorHelper.converBindError2AjaxError(bindingResult,outputAllPropError).toString());
            //return errorHelper.converBindError2AjaxError(bindingResult,outputAllPropError);
        } else {//正常輸出
            return pjp.proceed(args);
        }
    }
    
    /**
     * <p class="detail">
     * 功能:切點
     * </p>
     * @author Kings
     * @date 2016.08.03 17:51:42
     */
    @Pointcut ("@annotation(com.kings.common.validate.ValidateMethod)")
    public void validatePointcut() {
        
    }
    
    public void setOutputAllPropError(boolean outputAllPropError) {
        this.outputAllPropError = outputAllPropError;
    }
}

關於validate的就涉及到以上幾個類

下面上異常處理器

 

3.1.6 ResponseStatusAndBodyExceptionResolver

/**
 * <p class="detail">
 * 功能:針對ResponseStatus和ResponseBody的異常處理器,請在配置文件中將order設置爲-1覆蓋ResponseStatusExceptionResolver
 * </p>
 * @author Kings
 * @ClassName Response status and body exception resolver.
 * @Version V1.0.
 * @date 2016.08.09 15:23:54
 */
public class ResponseStatusAndBodyExceptionResolver extends AbstractHandlerExceptionResolver {
    
    /** Argument error. */
    private boolean argumentError = false;
    
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
        if (responseStatus != null) {
            try {
                return resolveResponseStatus(responseStatus, request, response, handler, ex);
            } catch (Exception resolveEx) {
                logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);
            }
        } else if (ex.getCause() instanceof Exception) {
            if (judgeInstance(ex)) {
                argumentError = true;
            }
            ex = (Exception) ex.getCause();
            return doResolveException(request, response, handler, ex);
        }
        
        //just Intercept the method @ResponseBody and @RestController or else skip
        ResponseBody rexist = ((HandlerMethod) handler).getMethod().getAnnotation(ResponseBody.class);
        RestController rcexist = ((HandlerMethod) handler).getBeanType().getAnnotation(RestController.class);
        
        if (rexist != null || rcexist != null) {
            try {
                HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;//默認500
                if (argumentError) {//參數錯誤400
                    status = HttpStatus.BAD_REQUEST;
                }
                response.setStatus(status.value());
    
                Object data;
                if (ex instanceof ValidateException) {//validateExcepption已經包含了錯誤的信息
                    data = JSONObject.fromObject(ex.getMessage());
                } else {
                    Map<String, Object> errorMap = new HashMap<>();
                    errorMap.put("error", ex.toString());
                    data = errorMap;// for json
                }
                RestResponse res = new RestResponse(status, data);
    
                Map<String, Object> map = new HashMap<>();//put error message
                map.put("error", res);
                return new ModelAndView("errorJsonView", map);
            } catch (Exception e) {
                logger.warn("error", e);
            } finally {
                argumentError = false;//release
            }
        }
        return null;
    }
    
    
    /**
     * <p class="detail">
     * 功能:
     * </p>
     * @param responseStatus :ResponseStatus
     * @param request        :請求
     * @param response       :響應
     * @param handler        :methodHandler
     * @param ex             :異常
     *
     * @return model and view
     * @throws Exception the exception
     * @author Kings
     * @date 2016.08.09 15:23:54
     */
    protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        int statusCode = responseStatus.code().value();
        
        response.setStatus(statusCode);
        Map<String, Object> map = new HashMap<>();
        Object data;
        if (ex instanceof ValidateException) {
            data = JSONObject.fromObject(ex.getMessage());
        } else {
            Map<String, Object> errorMap = new HashMap<>();
            errorMap.put("error", ex.toString());
            data = errorMap;// for json
        }
        map.put("error", data);
        return new ModelAndView("errorJsonView", map);//返回jsonView
    }
    
    private boolean judgeInstance(Exception ex) {
        return ex instanceof PropertyAccessException || ex instanceof ServletRequestBindingException;
    }
    
}

springmvc默認使用了ResponseStatusExceptionResolver來處理異常帶有@ResponseStatus的異常類,而且返回對應code的視圖。而rest在發生錯誤的時候,友好的形式是返回一個json視圖,而且說明錯誤的信息,這樣更加有利於在碰到異常的狀況下進行錯誤的定位,提升解決bug的效率。

咱們採用ResponseStatusAndBodyExceptionResolver,是對ResponseStatusExceptionResolver作了進一步處理,並做用在ResponseStatusExceptionResolver以前。ResponseStatusAndBodyExceptionResolver是針對加了@ResponseBody或者控制器加了@RestController的處理程序遇到異常的異常解析器,得到異常結果而且返回json(RestResponse)視圖

 

ResponseStatusExceptionResolver須要咱們在配置文件中加入配置

請看3.1.8中的配置

 

3.1.7 ErrorJsonView

/**
 * <p class="detail">
 * 功能:JsonView for error
 * </p>
 * @author Kings
 * @ClassName Error json view.
 * @Version V1.0.
 * @date 2016.08.09 15:17:39
 */
public class ErrorJsonView extends AbstractView {
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType("text/json; charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            Gson jb = new Gson();
            out.write(jb.toJson(model.get("error")));
            out.flush();
        } catch (IOException e) {
            logger.error("com.gttown.common.support.web.view.ErrorJsonView", e);
        }
    }
    
}

 

3.1.8 配置

<mvc:annotation-driven validator="validator"/>

    <!--驗證bean-->
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
        <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
        <!-- 若是不加默認到 使用classpath下的 ValidationMessages.properties -->
        <property name="validationMessageSource" ref="messageSource"/>
    </bean>

    <!-- 國際化的消息資源文件(本系統中主要用於顯示/錯誤消息定製) -->
    <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <!-- 在web環境中必定要定位到classpath 不然默認到當前web應用下找  -->
                <value>classpath:error</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
        <property name="cacheSeconds" value="60"/>
    </bean>

    <!--validate 切面-->
    <aop:aspectj-autoproxy />
    <bean class="com.kings.common.validate.ValidateHandelAspect">
        <!--outputAllPropError默認是false,將只輸出一個錯誤字段的信息,若是須要所有字段異常錯誤信息,那麼outputAllPropError設置爲true-->
        <property name="outputAllPropError" value="true"/>
    </bean>

    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="-1" /><!--這邊的order必需要大於咱們jsp等視圖模板的order-->
    </bean>
    <!--錯誤JsonView-->
    <bean id="errorJsonView" class="com.kings.template.mvc.view.ErrorJsonView"/>

    <!--responseStatus和responseBody異常處理器-->
    <bean id="responseStatusAndBodyExceptionResolver" class="com.kings.template.mvc.ResponseStatusAndBodyExceptionResolver">
        <property name="order" value="-1"/><!--負1用來覆蓋springmvc自帶的ResponseStatusExceptionResolver-->
    </bean>

 

 

3.1.9 控制器

@ValidateMethod
    @RequestMapping (value = "/errorhandler/2", method = RequestMethod.POST)
    public Person demo1(@Valid Person p, BindingResult bindingResult) {//BindingResult必須得寫,並且是緊跟在驗證明體以後,驗證的很少說了,就是得在方法體上加註解@ValidateMethod
        return p;
    }

    @RequestMapping (value = "/errorhandler/{id}", method = RequestMethod.GET)
    public String demo1(@PathVariable Long id) {
        return id.toString();
    }

 

3.1.10 效果

1.驗證

2.普通400

 

3.2 自定義頁面異常解析器

3.2.1 CustomerSimpleMappingExceptionResolver

/**
 * <p class="detail">
 * 功能:自定義異常處理類
 * </p>
 * @author Kings
 * @ClassName Custom simple mapping exception resolver.
 * @Version V1.0.
 * @date 2016.07.18 14:40:16
 */
public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {
    /** Logger. */
    private Logger logger = Logger.getLogger(CustomSimpleMappingExceptionResolver.class);
    
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        super.doResolveException(request, response, handler, ex);
        logger.error(ex.getMessage(), ex);
        String viewName = determineViewName(ex, request);
        if (viewName != null) {// JSP格式返回  
            if (! (request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")))) {
                // 若是不是異步請求  
                // Apply HTTP status code for error views, if specified.  
                // Only apply it if we're processing a top-level request.  
                Integer statusCode = determineStatusCode(request, viewName);
                if (statusCode != null) {
                    applyStatusCodeIfPossible(request, response, statusCode);
                }
                
                return getModelAndView(viewName, ex, request);
            } else {
                return null;
            }
        } else {
            return null;
        }
    }
}

 

3.2.2 配置

<!-- 統一異常處理 具備集成簡單、有良好的擴展性、對已有代碼沒有入侵性 -->
    <bean id="exceptionResolver" class="com.kings.common.resolver.CustomSimpleMappingExceptionResolver">
        <property name="defaultErrorView" value="/error/500"/>
        <property name="exceptionAttribute" value="ex"/>
        <property name="exceptionMappings">
            <props>
                <!-- 自定義業務異常 -->
                <prop key="com.gttown.common.support.exception.BizException">/error/biz</prop>
                <!-- 可再添加 -->
            </props>
        </property>
        <!-- 默認HTTP錯誤狀態碼 -->
        <property name="defaultStatusCode" value="500"/>
        <!-- 將路徑映射爲錯誤碼,供前端獲取。 -->
        <property name="statusCodes">
            <props>
                <prop key="/error/500">500</prop>
            </props>
        </property>
    </bean>

statusCodes須要web.xml error-code碼結合使用指向指定頁面

<error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/pages/error/500.jsp</location>
    </error-page>

 

3.3 ControllerAdvice

3.3.1 CustomerControllerAdvice

@ControllerAdvice
public class CustomerControllerAdvice {
    @ExceptionHandler (Exception.class)
    @ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public RestResponse handleBadRequestException(Exception ex) {
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("error",ex.toString());
        RestResponse response = new RestResponse(map);
        response.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setMessage("error");
        return response;
    }
   
}

經過ExceptionHandler指定哪些類型的錯誤執行具體某個返回錯誤方法

而且可使用@ResponseStatus執行錯誤代碼

注意在配置ControllerAdvice的時候,必須跟controller同樣在springmvc.xml配置掃描初始化

 

4.總結

在springmvc中咱們能夠有各類類型的異常解析器來統一處理異常,方便了咱們對異常的處理,經過在配置中加入異常處理的解析器,節約了控制器層的代碼,而且使得前端呈現出不一樣的響應code。

 

發現一個機制的導航😳

相關文章
相關標籤/搜索