以前写项目的时候所谓,异常直接在各个层里面捕捉处理后向外返回错误信息,但是实际上有些运行时异常很容易被忽略,这样就会导致这些异常出现后会直接返回给调用方。当时的解决方案就是在 Controller 层的每个方法加 try-catch
块,捕捉所有的异常并处理后返回给调用方,但是这样处理起来比较麻烦,因为每个方法上都要加 try-catch
,显得代码很不优雅。后来随着项目接触的多,有使用拦截器或者过滤器的,当然现在使用最多的还是切面,并且在 Spring Boot 中还提供了封装好的切面,实现即可。
1. 使用切面思想统一处理
使用切面的思想实现,主要的就两步,第一步是是编写切面的逻辑方法,第二步只要将切面作用到对应的控制层的方法上即可。当然在 Spring 中已经提供了对应实现封装,只要使用注解和配置即可,但是为了循序渐进的了解这个实现过程,还是先看一下原始的写法是什么样的。
1.1 原始写法
编写全局异常处理的实现类:
/*全局异常处理切面类*/
@Aspect
@Service
public class ExceptionAspectHandler {
@Pointcut(value = "@annotation(com.itcrud.jpa.annotaion.MyExceptionHandler)")
public void pointcut() {
}
@Around("pointcut()")
public Object exceptionHandle(ProceedingJoinPoint joinPoint) throws Throwable {
//TODO 可以获取请求参数进行打印操作
try {
Object object = joinPoint.proceed();//执行业务方法
//可以打印响应结果信息
System.out.println("响应信息:"+ object);
return object;
} catch (ServiceException e) {
//TODO 自定义异常处理逻辑
return new Object();//模拟异常处理结果返回
} catch (Exception e) {
//TODO 系统异常处理逻辑
return new Object();//模拟异常处理结果返回
}
}
}
上面的代码注释很清楚,注释写的也很详细,就是没有具体的业务处理代码,偷懒一下,有兴趣的自己补充。另外这里使用的注解方式实现,需要在控制层内的方法上加@MyExceptionHandler
注解。但是这种实现是不友好的,可以考虑将注解加到类上,也可以考虑使用表达式,在这里感觉表达式是最给力的。
自定义注解代码:
@Target({ElementType.METHOD}) //只能作用在方法上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExceptionHandler {
}
如果使用表达式,则是使用如下的写法:
execution(* com.itcrud.blog.*.controller..*.*(..))
大概说一下这个表达式,在 blog 和 controller 中间加了一个*
表示的是功能模块,如在一个大的 maven 项目下有 web、api 等,因为分类不同,这里的*
可能就不同,所以使用通配符更好,如果你的项目没有涉及到这种分类,那就可以不同考虑这个*
,直接去掉就好。
到这里一个原始的写法基本就是这样。
1.2 Spring 提供的方法
Spring 对于异常的处理切面给出了一套注解,以@ControllerAdvice
和@RestControllerAdvice
为主。下面来介绍一下。
@ControllerAdvice
和@RestControllerAdvice
是直接写在异常处理类上的,类似于上面的切面类,这两个注解的区别就是在于有没有@ResponseBody
。可以类比@Controller
和@RestController
之间的区别。@ExceptionHandler
是用在具体处理异常的方法上,可以通过 value 属性来指定此方法处理的异常类型。@ResponseStatus
是用在具体处理异常的方法上,指定响应给调用方的状态吗,如 404、500 等,状态码的指定使用HttpStatus
枚举类。@ResponseBody
可以用在处理异常的方法上,对响应的结果进行 json 的转换,如果在类上使用了@RestControllerAdvice
注解,方法上就不用加@ResponseBody
啦。(但是这里还是建议分开写,细化)
示例代码:
@ControllerAdvice
public class WebExceptionHandler {
/*自定义异常处理*/
@ExceptionHandler(value = ServiceException.class)
@ResponseBody
public Map<String,String> localHandle(){
return new HashMap<String, String>();
}
/*空指针异常处理*/
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String NullPointHandle(){
return "";
}
/*最终异常处理*/
@ExceptionHandler(Exception.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String exceptionHandle(){
return "";
}
}
上面的一段简洁代码就能实现,不需要在控制层内加任何注解,这个类会对被@RequestMapping
作用的方法自动拦截,实现异常的统一处理。
这种处理方式让我想起了一个问题,如果现在控制层里面定义了一个方法,这个方法是控制层中的一个公用方法,但是不处理外部的请求,使用之前原始写法的表达式方式就会出现问题,这个公用方法也会被切面切到,是不符合设计效果的,因此还是要在切面中去判断被切到的方法是否被@RequestMapping
系列的注解修饰。
总的来说,Spring 这个叫春的东西真的很好用,提供的功能都很强大。
2. 总结
在用原始写法的时候,一定要注意加上必要的判断,防止出现一些预想不到的问题。合理使用第三方提供的功能封装能够大大的提高开发的效率。
其实这个原始写法还是很有用的,正如我注释中写的,可以将请求的出参入参打印出来,另外的就是可以做日志编号的统一管理,实现日志链,方便在日志中查找。当然了这里也可以通过实现 Spring 的 HandlerInterceptor
接口,然后重写preHandle
、postHandle
以及afterCompletion
方法来实现。总感觉 Spring 什么地方都可以插一腿。