SpringMVC 异常处理

我们知道进行异常处理是 Controller 层的职责之一,但在每一个处理方法里使用try...catch块感觉又不太聪明的亚子...在 SpringMVC 中有 4 种常用方式,可以对异常进行统一处理:

  • 使用 SpringMVC 预定义的SimpleMappingExceptionResolver,发生指定异常后跳转页面;
  • 实现HandlerExceptionResolver,自定义异常处理器;
  • 使用注解@ExceptionHandler
  • 使用注解@ControllerAdvice配合@ExceptionHandler

下面来说说这 4 种方法的具体使用。

1. 配置 SimpleMappingExceptionResolver

只需要在 SpringMVC 配置文件中注册该异常处理器 Bean 即可,没有 id 属性,无需显式调用或被注入给其它 Bean。它对异常的处理方式仅限于跳转到指定的响应页面,在页面中进行处理。

示例:

// 一个自定义的异常类
public class MemberException extends Exception {
    public MemberException() {
        super();
    }
    public MemberException(String msg) {
        super(msg);
    }
}

配置指定异常和需要跳转的页面:

<!-- SpirngMVC配置文件 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <prop key="cn.cna.exception.MemberException">/errors/memberErrors.jsp</prop>
            <!-- 当请求参数的值与接收该参数的处理器方法形参的类型不匹配时,会抛出类型匹配异常TypeMismatchException -->
            <prop key="org.springframework.beans.TypeMismatchException">/errors/typeException.jsp</prop>
        </props>
    </property>
    <property name="defaultErrorView" value="/errors/defaultErrors.jsp"/>
    <property name="exceptionAttribute" value="ex"/>
</bean>

属性解释:

  • exceptionMappings:Properties 类型属性,用于指定不同类型的异常所对应的响应页面,Key 为异常类的全限定类名,value 为响应页面路径;
  • defaultErrorView:指定默认的异常响应页面,若发生的异常没有在 exceptionMappings 中指定,则使用该页面;
  • exceptionAttribute:捕获到的异常对象,可以在异常响应页面中使用。

这样,Controller 方法可以不用显式进行异常处理,而是通过添加throws异常声明,将异常处理的职责交给上层调用者,进而转交给注册好的 SimpleMappingExceptionResolver,根据不同的异常实例进行不同的页面响应。

2. 实现 HandlerExceptionResolver

使用第 1 种方法可以实现发生指定异常后的跳转,但如果想要在捕获异常后执行其他的操作,就需要自己实现HandlerExceptionResolver接口,并同样在 SpringMVC 配置文件中注册。

HandlerExceptionResolver只有一个resolveException方法需要我们去实现,来看看代码:

public class MemberExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
        ModelAndView mv = new ModelAndView();
        // 将异常对象加入数据模型中
        mv.addObject("ex",ex);
        // 设置默认错误响应页面
        mv.setViewName("/errors/defaultErrors.jsp");
        // 设置MemberException响应页面
        if (ex instanceof MemberException) {
            mv.setViewName("/errors/memberErrors.jsp");
        }
        // 其他...
        return mv;
    }
}

在 SpringMVC 配置文件中注册:

<bean class="cn.cna.resolver.MyExceptionResolver"/>

你可能有个疑问,如果项目采用的是前后端分离,需要返回 Json 数据又该如何做?有两个方法:

  1. 用 response 的 writer 直接输出 Json 数据
@Override
public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
    // 避免中文乱码
    response.setContentType("application/json;charset=UTF-8");
    response.setCharacterEncoding("UTF-8");
    try {
        // 为了简化演示,这里省略了对象转Json字符串的操作
        String jsonStr = "";
        if (ex instanceof MemberException) {
            response.setStatus(HttpStatus.OK.value());
            jsonStr = "{'code':1,'message':'业务异常'}";
       } else {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            jsonStr = "{'code':500,'message':'服务器未知异常'}";
        }
        response.getWriter().print(jsonStr);
        response.getWriter().flush();
        response.getWriter().close();  // 关闭了输出,确保后面不会再使用该response,比如拦截器
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;  // 后续不会有渲染步骤,但若还有处理器将继续执行
}
  1. 借助 MappingJackson2JsonView

MappingJackson2JsonView是一个 Json 视图,用法如下:

@Override
public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
    ModelAndView mv = new ModelAndView();
    MappingJackson2JsonView view = new MappingJackson2JsonView();
    view.setJsonPrefix("fsxJson"); // 设置 Json 前缀
    // view.setModelKey(); 只序列化指定的key
    mv.setView(view);
    // 添加key-value非常方便
    mv.addObject("code", "1");
    mv.addObject("message", "业务异常");
    return mv;
}

详细可见:@ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】,YourBatman,CSDN,写得非常深入的一篇文章,值得好好研究。

3. 使用 @ExceptionHandler

@ExceptionHandler是 Spring 3.0 后提供的处理异常的注解,目的在于对 REST 应用提供更多的支持(是的,ModelAndView不再是必须的)。被@ExceptionHandler标注的方法将成为一个异常处理器,通过value属性(类型是一个Class<?>数组)指定该方法所要匹配的异常。

// org.springframework.web.bind.annotation.ExceptionHandler

@Target(ElementType.METHOD) // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    // 指定异常类型,可以多个
    Class<? extends Throwable>[] value() default {};
}

使用:

//MemberException异常处理
@ExceptionHandler(MemberException.class)
public ApiResult handleMemberException(MemberException ex){
    return ApiResult.error().setMessage(ex.getMessage());
}

但是请注意,被@ExceptionHandler注解的方法只能处理同一个 Controller 中的其他方法产生的异常。一般会将异常处理方法专门定义在一个 Controller 中,让其它 Controller 继承它。但是 Java 是单继承的呀,如果基类 Controller 中还需要封装其他逻辑,就会显得臃肿,职责也不够明确。

@ControllerAdvice + @ExceptionHandler

@ControllerAdvice是 Spring3.2 提供的新注解,基于 AOP 思想,可以指定一个 Controller 增强器,配合@ExceptionHandler可用于全局的异常处理,现在不必纠结于继承了。

// 全局异常处理
// 如果是REST应用,就使用@RestControllerAdvice,不用给每个方法写@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(MemberException.class)
    public ApiResult handleMemberException(MemberException ex) {
        return ApiResult.error().setMessage(ex.getMessage());
    }
    // 添加其他异常处理方法...
}

5. 关于优先级

从高到低:

  • @Controller + @ExceptionHandler
  • @ControllerAdvice + @ExceptionHandler
  • HandlerExceptionResolver

三种方式并存的情况下,优先级越高的先选择,被捕获处理了的异常与后面无关,没有被捕获的就会继续传递。

为了避免在处理异常的过程中又产生异常的情况,一个好的办法是设计一个 HandlerExceptionResolver 放在末位,用最不会出 bug 的代码来处理一切前面不能处理的异常。

END

参考:
springmvc异常处理,JS_HCX,简书
@ControllerAdvice实现优雅地处理异常,KEN DO EVERTHING,CSDN
Spring 异常处理三种方式,喜欢日向雏田一样的女子啊,CSDN

posted @ 2020-03-11 23:40  逆风的小飞侠  阅读(207)  评论(0编辑  收藏  举报