01、背景介绍
在上文中,我们介绍了在 Spring Boot 中实现接口数据格式的统一返回处理实现,其中就包括程序运行时的异常处理,通过全局异常处理器,可以简化代码逻辑,统一响应格式。
其实在 Spring Boot 中,针对controller层的异常处理有很多种办法。今天通过这篇文章,我们就一起来总结一下相关异常处理的实现方式。
02、方案实践
在 Spring Boot 中针对controller层的异常处理,有两种常用实现方式,都可以达到简化代码逻辑的效果。
- 方式一:通过@ControllerAdvice和@ExceptionHandler注解实现全局异常的处理
- 方式二:通过实现HandlerExceptionResolver接口来完成全局异常的处理
下面我们一起来看看具体实现。
2.1、全局异常处理方式一
通过@ControllerAdvice和@ExceptionHandler注解实现全局异常拦截,在之前的文章中我们有多次介绍过,它可以拦截controller层请求方法抛出的异常信息,同时外加@ ResponseBody注解,可以实现响应类型为json格式。
例如,现在有两种异常类型NullPointerException和Exception,分别对其进行捕捉,具体实现如下!
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 优先处理空指针异常
* @param e
* @return
*/
@ExceptionHandler(value = {NullPointerException.class})
@ResponseBody
public Object nullPointerExceptionHandler(HttpServletRequest request, NullPointerException e){
LOGGER.error("发生空指针异常,请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(500, e.getMessage());
}
/**
* 兜底处理其它异常
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知异常,请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(999, e.getMessage());
}
}
测试代码,如下:
@RestController
public class HelloController {
@GetMapping(value = "/add")
public String hello(){
if(1 ==1){
throw new NullPointerException("空指针测试");
}
return "hello world";
}
@GetMapping(value = "/delete")
public String delete(){
if(1 ==1){
throw new RuntimeException("其它测试");
}
return "hello world";
}
}
启动服务后,在浏览器中请求http://localhost:8080/add,结果如下:
图片
请求http://localhost:8080/delete,结果如下:
图片
结果与预期一致。
2.1.1、自定义异常类实现
很多场景下,我们希望通过自定义异常类来返回相关错误信息,如何实现呢?
首先自定义一个异常类CustomerException。
public class CustomerException extends RuntimeException {
private Integer code;
public Integer getCode() {
return code;
}
public CustomerException(String message) {
super(message);
this.code = 500;
}
public CustomerException(Integer code, String message) {
super(message);
this.code = code;
}
}
然后,在全局异常处理器中增加相关的捕捉方法。
/**
* 处理自定义的异常
* @param e
* @return
*/
@ExceptionHandler(value = {CustomerException.class})
@ResponseBody
public Object customerExceptionHandler(HttpServletRequest request, CustomerException e){
LOGGER.error("发生业务异常,请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(e.getCode(), e.getMessage());
}
测试代码,如下:
@GetMapping(value = "/update")
public String update(){
if(1 ==1){
throw new CustomerException(4003, "请求ID不能为空");
}
return "hello world";
}
启动服务后,在浏览器中请求http://localhost:8080/update,结果如下:
图片
结果与预期一致!
2.1.2、404 异常特殊处理
默认情况下,@ExceptionHandler注解无法捕捉到 404 异常,比如请求一个无效的地址,返回信息如下:
图片
如果想要捕捉到这种异常,可以在application.properties文件中添加如下配置来实现。
# 如果没有找到请求地址,抛异常
spring.mvc.throw-exception-if-no-handler-found=true
# 关闭默认的静态资源路径映射
spring.resources.add-mappings=false
启动服务后,再次发起地址请求,结果如下:
图片
对于前后端分离开发的情况,这种方式非常实用;但是如果前后端不分离的项目,比如访问项目/static目录下的静态资源,可能前端无法正常访问。
此时,我们可以手动添加资源映射,比如如下操作,前端就能正常访问静态资源了。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 允许访问localhost:8080/static/目录下的静态资源
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
2.1.3、自定义异常页面实现
某些场景下,当发生异常时希望跳转到自定义的异常页面,如何实现呢?
首先,这里基于thymeleaf模板引擎来开发页面,在templates目录下创建一个异常页面error.html。
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
出错啦,请与管理员联系<br>
<span th:text="${message}"></span>
</body>
</html>
然后,修改异常捕捉方法,这里无需添加@ResponseBody注解,示例如下。
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 跳转到异常页面
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知异常,请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
ModelAndView mv = new ModelAndView();
// 添加错误信息对象
mv.addObject("message", e.getMessage());
// 要跳转的页面视图
mv.setViewName("error");
return mv;
}
}
启动服务后,在浏览器中再次请求http://localhost:8080/update,结果如下:
图片
结果与预期一致!
2.1.4、RestControllerAdvice和ControllerAdvice的区别
很多同学喜欢用@RestControllerAdvice来代替@ControllerAdvice和@ResponseBody。
例如如下示例!
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理所有异常,以json方式响应
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
public Object exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知异常,请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(999, e.getMessage());
}
}
实现效果,与上文等价。
打开@RestControllerAdvice的源码,你会发现它将@ControllerAdvice和@ResponseBody注解组合在一起了,因此同时具备两者效果,部分源码如下:
package org.springframework.web.bind.annotation;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
2.2、全局异常处理方式二
在 Spring Boot 中,除了通过@ControllerAdvice和@ExceptionHandler注解实现全局异常处理外,还有一种通过实现HandlerExceptionResolver接口来完成全局异常的处理。
具体实现示例如下:
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionResolver.class);
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
LOGGER.error("接口请求出现异常,请求地址:{},错误信息:{}", request.getRequestURI(), e.getMessage());
if(e instanceof RuntimeException){
// 设置响应类型为json格式
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("code", 500);
mv.addObject("msg", e.getMessage());
return mv;
} else {
// 设置响应类型为错误页面
ModelAndView mv = new ModelAndView();
mv.addObject("message", e.getMessage());
mv.setViewName("error");
return mv;
}
}
}
当出现异常的时候,结果会以json格式响应给客户端。
启动服务后,发起地址请求,结果如下:
这种思路的实现原理,主要是通过 SpringMVC 的异常处理链路器来完成异常的全局处理。
SpringMVC 支持用户自定义异常处理类(需要实现HandlerExceptionResolver),当发生异常时,默认异常处理类无法处理时,就会交给自定义异常处理类来完成。实现方面比较灵活,即可以实现以json格式响应,也可以以页面视图的方式响应。
虽然这种方式能够处理全局异常,但是 Spring 官方不推荐使用它;同时实测过程中发现它无法拦截 404 错误,当请求错误地址时,会优先被DefaultHandlerExceptionResolver默认异常处理类拦截,自定义的异常处理类无法捕捉。
03、小结
最后总结一下,虽然方式一和方式二都可以实现controller层接口请求异常的全局处理,但是在实际使用中,推荐方式一,简单好维护。
示例代码地址:
https://gitee.com/pzblogs/spring-boot-example-demo