Springboot 全局异常处理+统一后端响应(附源码)
前言
本文主要介绍SpringBoot前后端分离开发模式下,如何友好与前端进行交互。包含controller层统一返回处理,全局异常捕获。
统一后端响应
定义返回格式

1 @Getter 2 @AllArgsConstructor 3 @NoArgsConstructor 4 public class ResultVo { 5 6 private int code; 7 8 private String msg; 9 10 private Object data; 11 12 13 // 默认返回成功状态码,数据对象 14 public ResultVo(Object data) { 15 this.code = ResultCode.SUCCESS.getCode(); 16 this.msg = ResultCode.SUCCESS.getMsg(); 17 this.data = data; 18 } 19 20 // 返回指定状态码,数据对象 21 public ResultVo(StatusCode statusCode, Object data) { 22 this.code = statusCode.getCode(); 23 this.msg = statusCode.getMsg(); 24 this.data = data; 25 } 26 27 public ResultVo(ResultCode status, String msg, Object data) { 28 this.code = status.getCode(); 29 this.msg = msg; 30 this.data = data; 31 } 32 33 public static ResultVo success(Object data) { 34 return new ResultVo(ResultCode.SUCCESS, data); 35 } 36 37 public static ResultVo fail(ResultCode status, String msg, Object data) { 38 return new ResultVo(status, msg, data); 39 } 40 }
统一状态码

1 @Getter 2 public enum ResultCode implements StatusCode { 3 SUCCESS(1, "请求成功"), 4 FAIL(-1, "操作失败"), 5 6 ERROR_500(500, "服务器未知错误"), 7 ERROR_400(400, "错误请求"); 8 9 private int code; 10 private String msg; 11 12 ResultCode(int code, String msg) { 13 this.code = code; 14 this.msg = msg; 15 } 16 }
测试代码

1 @GetMapping("/response/demo") 2 public ResultVo paramValid() { 3 return new ResultVo(User.builder().id("1234").accountId("LS998").name("ls").build()); 4 }
至此,后端返回基本完成,效果如图
问题:如此配置,每次都需要后端开发new ResultVo(),重复工作太多。
解决:Springboot提供了@RestControllerAdvice注解进行控制器全局配置,默认控制全部controller,可通过参数basePackages进行某个包路径下controller控制。

1 @RestControllerAdvice(basePackages = "com.ls.code.valid") 2 public class BaseResponseBodyAdvice implements ResponseBodyAdvice<Object> { 3 4 @Override 5 public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) { 6 return true; 7 } 8 9 @Override 10 @SneakyThrows 11 public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { 12 return ResultVo.success(data); 13 } 14 }
测试代码:

1 @GetMapping("/response/demo") 2 public User paramValid() { 3 return User.builder().id("1234").accountId("LS998").name("ls").build(); 4 }
效果如图,自动封装返回结构
问题1:如果某些开发,在此配置下,又在控制器里写了new Result(),会如何?
测试代码

1 @GetMapping("/response/demo") 2 public ResultVo paramValid() { 3 return new ResultVo(User.builder().id("10086").name("ls").build()); 4 }
效果如图:显然需要对这种情况再次处理,因为没法保证所有开发都和你一样懒
问题2:如果返回String类型会如何
测试代码

1 @GetMapping("/response/demo") 2 public String paramValid() { 3 return "测试代码"; 4 }
效果如图:内部报错为java.lang.ClassCastException: com.ls.code.valid.common.ResultVo cannot be cast to java.lang.String,显然需要对String类型的返回值进行处理。
问题3:业务中是否有需求为 只返回一个字符串,并不需要和前端交互。如:各种中间件的探活等。只需要返回字符串即可。
解决方案为 将探活接口controller放在全局控制器RestControllerAdvice外。但是不建议这样做,不优美。更好的做法为定义注解。对不需要控制的接口进行单独处理。
最终控制类代码如下:

1 @RestControllerAdvice(basePackages = "com.ls.code.valid") 2 public class BaseResponseBodyAdvice implements ResponseBodyAdvice<Object> { 3 4 @Autowired 5 private ObjectMapper objectMapper; 6 7 8 /** 9 * @param methodParameter 方法返回的类型 10 * @param converterType 参数类型装换 11 * @return 返回true 则调用 beforeBodyWrite方法 12 */ 13 @Override 14 public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) { 15 //返回类型为ResultVo 或者带有IgnoreResponseAdvice注解 则不进行包装 16 return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class) 17 || methodParameter.hasMethodAnnotation(IgnoreResponseAdvice.class)); 18 } 19 20 @Override 21 @SneakyThrows 22 public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { 23 //String类型返回值单独处理 24 if (data instanceof String) { 25 return objectMapper.writeValueAsString(new ResultVo(data)); 26 } 27 return ResultVo.success(data); 28 } 29 }
全局异常处理
Springboot提供@ControllerAdvice注解,可以进行全局异常定制
我们所需处理的异常无非三种类型
1:各种参数校验异常,例:账号密码不能为空等 代码以spring-boot-starter-validation为例
如果不进行处理,效果如图
系统内部报错异常为BindException,对BindException异常进行处理。
代码如下:

1 @ControllerAdvice 2 @ResponseBody 3 public class ControllerExceptionAdvice { 4 /** 5 * 参数校验异常捕获 6 * 7 * @param e 8 * @return 9 */ 10 @ExceptionHandler({BindException.class}) 11 public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) { 12 // 从异常对象中拿到ObjectError对象 13 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); 14 return new ResultVo(ResultCode.ERROR_400, objectError.getDefaultMessage()); 15 } 16 }
效果如图
2:自定义业务异常,开发中,经常要对用户的错误操作进行提示等,需要我们自定义异常,并给出相应的提示 例:商品已下架等
自定义异常代码

1 @Getter 2 public class BusinessException extends RuntimeException { 3 private Integer code; 4 private String msg; 5 6 public BusinessException(ResultCode resultCode, String message) { 7 //设置错误详情 8 super(message); 9 this.code = resultCode.getCode(); 10 this.msg = resultCode.getMsg(); 11 } 12 13 public BusinessException(String message) { 14 super(message); 15 this.code = ResultCode.FAIL.getCode(); 16 this.msg = ResultCode.FAIL.getMsg(); 17 } 18 }
对自定义异常进行响应封装

1 /** 2 * 业务异常捕获 3 * 4 * @param e 5 * @return 6 */ 7 @ExceptionHandler({BusinessException.class}) 8 public ResultVo MethodArgumentNotValidExceptionHandler(BusinessException e) { 9 return new ResultVo(e.getCode(), e.getMsg(), e.getMessage()); 10 }
3:服务器异常,例:空指针 。由于异常种类众多,我们不能对每一种异常都做定制化,所以只对Exception进行封装,异常由开发自己保证,而且这些异常返回给前端并没有什么意义。
代码如下:

1 @ExceptionHandler({Exception.class}) 2 public ResultVo MethodArgumentNotValidExceptionHandler(Exception e) { 3 log.error(Throwables.getStackTrace(e)); 4 return ResultVo.fail(ResultCode.ERROR_500, ResultCode.ERROR_500.getMsg(), e.getMessage()); 5 }
源代码已经上传:https://gitee.com/liushuai2716/code.git
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?