遨游springmvc之HandlerExceptionReso

作者: 紫霞等了至尊宝五百年 | 来源:发表于2018-04-23 13:02 被阅读27次

    1.前言

    在我们的程序中,很多时候会碰到对异常的处理,我们也许会定义一些自己特殊业务的异常,在发生错误的时候会抛出异常,在springmvc的实际应用中,我们经常需要返回异常的信息以及错误代码,并且对异常进行一些处理然后返回再返回视图。这就要涉及到我们这一篇主要讲的HandlerExceptionResolver

    2.原理

    其实springmvc已经默认给我们注入了3个异常处理的解器:

    AnnotationMethodHandlerExceptionResolver(针对@ExceptionHandler,3.2已废除,转而使用ExceptionHandlerExceptionResolver)
    ResponseStatusExceptionResolver(针对加了@ResponseStatus的exception)
    DefaultHandlerExceptionResolver(默认异常处理器)

    2.1 依赖

    2.1.1 解析器依赖

    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,方法体中包含处理的方法,异常已经请求和响应参数。

    在我们自己去实现自定义异常解析器的时候,我们一般是去继承AbstractHandlerExceptionResolver

    AbstractHandlerExceptionResolver实现了HandlerExceptionResolver和Ordered

    那么针对异常的处理具体是在哪里执行的呢?

    答案是springmvc核心类DispatcherServlet

    在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的一个标准实体

    /**
     * 功能:REST接口标准容器
     * @param <T> the type parameter
     * @ClassName Rest response.
     */
    @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

    
    @Retention (RetentionPolicy.RUNTIME)
    @Target (ElementType.METHOD)
    public @interface ValidateMethod {
    
    }
    

    3.1.3 ValidateException

    /**
     * 功能:验证异常
     */
    @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

    /**
     * 功能:验证切面
     */
    @Aspect
    public class ValidateHandelAspect {
        /**judge is all property error need to be export*/
        private boolean outputAllPropError = false;
    
    
         * 功能:验证输出结果
    
        @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);
            }
        }
    
        /**
         * 功能:切点
         */
        @Pointcut ("@annotation(com.kings.common.validate.ValidateMethod)")
        public void validatePointcut() {
    
        }
    
        public void setOutputAllPropError(boolean outputAllPropError) {
            this.outputAllPropError = outputAllPropError;
        }
    }
    

    关于validate的就涉及到以上几个类

    下面上异常处理器

    3.1.6 ResponseStatusAndBodyExceptionResolver

    /**
     * 功能:针对ResponseStatus和ResponseBody的异常处理器,请在配置文件中将order设置为-1覆盖ResponseStatusExceptionResolver
     */
    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;
        }
    
        /**
         * @param responseStatus :ResponseStatus
         * @param request        :请求
         * @param response       :响应
         * @param handler        :methodHandler
         * @param ex             :异常
         */
        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

    /**
     * 功能:JsonView for error
     */
    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.验证

    [图片上传失败...(image-ca1aec-1524459183218)]

    2.普通400

    [图片上传失败...(image-2a27a9-1524459183218)]

    3.2 自定义页面异常解析器

    3.2.1 CustomerSimpleMappingExceptionResolver

    /**
     * 功能:自定义异常处理类
     */
    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。

    相关文章

      网友评论

        本文标题:遨游springmvc之HandlerExceptionReso

        本文链接:https://www.haomeiwen.com/subject/vgoklftx.html