在Spring Boot/Cloud项目中,统一全局返回格式和全局异常处理是项目开发的标配,它能极大减少重复代码、降低前后端对接成本、提升问题排查效率。
今天就给大家带来一套可以直接复制粘贴、稍作修改就能上线的优雅实现方案。
一、先搞懂核心理论
1. 核心注解说明
@RestControllerAdvice:这是Spring提供的增强注解,是@ControllerAdvice+@ResponseBody的组合,专门用于处理Rest风格接口的全局增强逻辑(返回值包装、异常捕获),默认作用于所有@RestController标注的控制器。ResponseBodyAdvice:接口,用于对Controller返回的结果进行统一包装处理,无需在每个接口手动构建返回对象。@ExceptionHandler:用于标注异常处理方法,指定该方法处理某一种或多种异常类型,配合@RestControllerAdvice实现全局异常捕获。
2. 整体架构设计
我们将实现3个核心组件,形成完整的全局处理链路:
- 统一返回结果封装类(基础载体,前后端数据传输的标准格式)
- 全局返回值增强器(自动包装所有接口返回结果)
- 全局异常处理器(捕获所有未手动处理的异常,统一返回异常信息)
- 配套辅助类(状态码枚举、自定义业务异常,提升扩展性)
二、代码实现(直接复制可用)
步骤1:统一返回结果封装类(Result.java)
这是前后端数据交互的标准格式,所有接口(正常返回/异常返回)都将通过该类返回,消除格式不一致的问题。
importlombok.Data;importjava.io.Serializable;importjava.time.LocalDateTime;/** * 全局统一返回结果封装类 * 说明:所有接口返回结果必须通过此类包装,前后端严格按照该格式进行数据交互 */@DatapublicclassResult<T>implementsSerializable{privatestaticfinallongserialVersionUID=1L;// 响应状态码(200成功、500系统异常、自定义业务异常码等)privateIntegercode;// 响应消息(成功/失败提示信息)privateStringmsg;// 响应数据(正常返回时携带的业务数据,异常时可置为null)privateTdata;// 响应时间戳(方便排查问题,记录接口返回时间)privateLocalDateTimetimestamp;// 私有化构造方法,禁止外部直接创建,统一通过静态方法构建privateResult(){this.timestamp=LocalDateTime.now();}// ==================== 静态工具方法:构建返回结果 ====================/** * 构建成功返回结果(携带业务数据) */publicstatic<T>Result<T>success(Tdata){Result<T>result=newResult<>();result.setCode(ResultCode.SUCCESS.getCode());result.setMsg(ResultCode.SUCCESS.getMsg());result.setData(data);returnresult;}/** * 构建成功返回结果(不携带业务数据,适用于新增/修改/删除等操作) */publicstatic<T>Result<T>success(){returnsuccess(null);}/** * 构建失败返回结果(自定义状态码和消息) */publicstatic<T>Result<T>fail(Integercode,Stringmsg){Result<T>result=newResult<>();result.setCode(code);result.setMsg(msg);result.setData(null);returnresult;}/** * 构建失败返回结果(基于状态码枚举) */publicstatic<T>Result<T>fail(ResultCoderesultCode){returnfail(resultCode.getCode(),resultCode.getMsg());}// ==================== 【需要修改/扩展】:如果项目有固定的成功提示语,可在这里自定义 ====================}步骤2:状态码枚举类(ResultCode.java)
统一管理项目中的所有状态码,避免硬编码,提升可维护性,后续新增状态码直接在枚举中添加即可。
/** * 全局统一状态码枚举 * 说明: * 1. 遵循HTTP状态码规范,2xx表示成功,4xx表示客户端异常,5xx表示服务端异常 * 2. 业务异常码可在基础状态码上进行扩展(如:40001表示参数校验失败,50001表示业务逻辑异常) */publicenumResultCode{// ==================== 基础状态码 ====================SUCCESS(200,"操作成功"),SYSTEM_ERROR(500,"系统内部异常,请稍后重试"),PARAM_ERROR(400,"请求参数格式不正确"),NOT_FOUND(404,"请求资源不存在"),METHOD_NOT_ALLOWED(405,"请求方式不支持"),// ==================== 【需要修改/扩展】:业务状态码(根据项目需求添加) ====================BUSINESS_ERROR(50001,"业务逻辑异常"),USER_NOT_EXIST(40001,"用户不存在"),TOKEN_EXPIRED(40101,"登录令牌已过期");// 状态码privatefinalIntegercode;// 状态描述privatefinalStringmsg;ResultCode(Integercode,Stringmsg){this.code=code;this.msg=msg;}// getter方法publicIntegergetCode(){returncode;}publicStringgetMsg(){returnmsg;}}步骤3:自定义业务异常类(BusinessException.java)
项目开发中,我们经常需要手动抛出业务异常(如:用户不存在、订单已关闭),该类专门用于封装业务异常信息,与系统异常区分开,方便精准处理和排查。
/** * 自定义业务异常类 * 说明:用于抛出业务逻辑相关的异常,由全局异常处理器专门捕获处理 */publicclassBusinessExceptionextendsRuntimeException{// 业务异常码privateIntegercode;// ==================== 构造方法 ====================publicBusinessException(ResultCoderesultCode){super(resultCode.getMsg());this.code=resultCode.getCode();}publicBusinessException(Integercode,Stringmsg){super(msg);this.code=code;}publicBusinessException(Stringmsg){super(msg);this.code=ResultCode.BUSINESS_ERROR.getCode();}// ==================== getter方法 ====================publicIntegergetCode(){returncode;}}步骤4:全局返回值增强器(ControllerResponseAdvice.java)
实现ResponseBodyAdvice接口,自动包装所有@RestController接口的返回结果,无需在每个接口手动调用Result.success(),极大减少重复代码。
importcom.fasterxml.jackson.core.JsonProcessingException;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.core.MethodParameter;importorg.springframework.http.MediaType;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.http.server.ServerHttpRequest;importorg.springframework.http.server.ServerHttpResponse;importorg.springframework.web.bind.annotation.RestControllerAdvice;importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;importjavax.annotation.Resource;/** * 全局返回值增强器 * 说明:自动将所有RestController接口的返回结果包装为统一的Result格式 */@RestControllerAdvice(// 【需要修改】:指定当前项目的Controller包路径(必填,避免作用于第三方包的接口)basePackages="com.example.demo.controller")publicclassControllerResponseAdviceimplementsResponseBodyAdvice<Object>{// 注入Jackson对象转换器,用于处理String类型返回值的特殊情况@ResourceprivateObjectMapperobjectMapper;/** * 判断当前返回结果是否需要进行包装处理(返回true表示需要包装) */@Overridepublicbooleansupports(MethodParameterreturnType,Class<?extendsHttpMessageConverter<?>>converterType){// 核心逻辑:排除已经是Result类型的返回结果,避免重复包装return!returnType.getMethod().getReturnType().isAssignableFrom(Result.class);}/** * 对返回结果进行包装处理(核心方法) */@OverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,Class<?extendsHttpMessageConverter<?>>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){// 1. 处理String类型返回值(特殊情况:String类型会被StringHttpMessageConverter处理,直接返回Result会报错)if(bodyinstanceofString){try{returnobjectMapper.writeValueAsString(Result.success(body));}catch(JsonProcessingExceptione){thrownewRuntimeException("String类型返回值包装失败",e);}}// 2. 处理null值(避免返回null,统一包装为成功状态的空数据)if(body==null){returnResult.success();}// 3. 正常包装:将返回结果封装为Result.success(body)returnResult.success(body);}}步骤5:全局异常处理器(GlobalExceptionAdvice.java)
捕获项目中所有未手动处理的异常,统一封装为Result格式返回,避免前端收到晦涩的异常堆栈信息,同时方便后端排查问题。
importlombok.extern.slf4j.Slf4j;importorg.springframework.web.bind.MethodArgumentNotValidException;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.RestControllerAdvice;importorg.springframework.web.servlet.NoHandlerFoundException;/** * 全局异常处理器 * 说明:捕获所有RestController接口抛出的未处理异常,统一返回格式化的异常信息 * 异常处理顺序:子类异常在前,父类异常在后(Spring会优先匹配最具体的异常类型) */@Slf4j@RestControllerAdvice(// 【需要修改】:与ControllerResponseAdvice保持一致,指定项目的Controller包路径basePackages="com.example.demo.controller")publicclassGlobalExceptionAdvice{/** * 捕获自定义业务异常(优先级最高,因为是我们手动抛出的,最具体) */@ExceptionHandler(BusinessException.class)publicResult<?>handleBusinessException(BusinessExceptione){// 打印业务异常日志(级别为warn,方便区分系统异常)log.warn("业务异常:{}",e.getMessage(),e);returnResult.fail(e.getCode(),e.getMessage());}/** * 捕获请求参数校验异常(如:@NotBlank、@NotNull等注解校验失败) */@ExceptionHandler(MethodArgumentNotValidException.class)publicResult<?>handleMethodArgumentNotValidException(MethodArgumentNotValidExceptione){log.warn("参数校验异常:{}",e.getMessage(),e);// 提取第一个参数校验失败的提示信息(返回给前端,提升用户体验)StringerrorMsg=e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();returnResult.fail(ResultCode.PARAM_ERROR.getCode(),errorMsg);}/** * 捕获404异常(资源不存在) */@ExceptionHandler(NoHandlerFoundException.class)publicResult<?>handleNoHandlerFoundException(NoHandlerFoundExceptione){log.error("404异常:{}",e.getMessage(),e);returnResult.fail(ResultCode.NOT_FOUND);}/** * 捕获所有未处理的系统异常(兜底处理,父类Exception) */@ExceptionHandler(Exception.class)publicResult<?>handleException(Exceptione){// 系统异常打印error级别日志,方便排查问题(包含完整堆栈信息)log.error("系统内部异常",e);// 注意:返回给前端的是友好提示,不返回具体异常信息(避免泄露系统细节)returnResult.fail(ResultCode.SYSTEM_ERROR);}// ==================== 【需要扩展】:根据项目需求,可添加更多异常处理方法(如:NullPointerException、IOException等) ====================}三、关键修改点标注
以上代码复制到项目后,只需要修改以下3处,即可快速上线:
@RestControllerAdvice的basePackages属性:将com.example.demo.controller修改为你项目中实际的Controller包路径(如:com.xxx.project.user.controller),确保只作用于当前项目的控制器,避免影响第三方依赖。ResultCode枚举类:根据项目业务需求,新增/修改业务状态码(如:订单相关、支付相关的异常码),删除无用的状态码。- (可选)异常处理扩展:如果项目中有特殊异常需要单独处理(如:Redis连接异常、数据库连接异常),在
GlobalExceptionAdvice中新增对应的@ExceptionHandler方法即可。
四、实战场景演示
我们创建一个测试Controller,验证全局返回值增强和全局异常处理的效果,大家可以直接复制该Controller进行测试。
importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.validation.constraints.NotBlank;/** * 测试Controller:验证全局返回值增强和全局异常处理 */@RestController@RequestMapping("/test")publicclassTestController{/** * 场景1:正常返回(携带业务数据) * 预期结果:自动包装为 Result{code=200, msg='操作成功', data='Hello World', timestamp=xxx} */@GetMapping("/normal")publicStringnormal(){return"Hello World";}/** * 场景2:正常返回(不携带业务数据) * 预期结果:自动包装为 Result{code=200, msg='操作成功', data=null, timestamp=xxx} */@GetMapping("/empty")publicvoidempty(){// 无返回值}/** * 场景3:抛出自定义业务异常 * 预期结果:捕获异常,返回 Result{code=40001, msg='用户不存在', data=null, timestamp=xxx} */@GetMapping("/business/{userId}")publicvoidbusinessException(@PathVariableStringuserId){if("10086".equals(userId)){thrownewBusinessException(ResultCode.USER_NOT_EXIST);}}/** * 场景4:抛出系统异常(空指针异常,未手动处理) * 预期结果:捕获兜底异常,返回 Result{code=500, msg='系统内部异常,请稍后重试', data=null, timestamp=xxx} */@GetMapping("/system")publicvoidsystemException(){Stringstr=null;str.length();// 手动制造空指针异常}/** * 场景5:参数校验异常(@NotBlank注解校验失败) * 预期结果:捕获参数异常,返回 Result{code=400, msg='姓名不能为空', data=null, timestamp=xxx} */@GetMapping("/param")publicvoidparamException(@NotBlank(message="姓名不能为空")Stringname){}}五、使用效果与注意事项
1. 预期效果
无论接口正常返回还是抛出异常,前端收到的都是格式统一的JSON数据,示例如下:
// 正常返回{"code":200,"msg":"操作成功","data":"Hello World","timestamp":"2026-01-15T15:30:00"}// 业务异常返回{"code":40001,"msg":"用户不存在","data":null,"timestamp":"2026-01-15T15:31:00"}2. 注意事项
- 依赖说明:该方案使用了
lombok(@Data注解),如果项目未引入lombok,需要手动添加getter/setter方法,或在pom.xml中引入lombok依赖:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version><scope>provided</scope></dependency>- 404异常捕获:需要在application.yml中添加配置,开启404异常抛出:
spring:mvc:throw-exception-if-no-handler-found:trueweb:resources:add-mappings:false- 避免重复包装:
ControllerResponseAdvice的supports方法已经排除了Result类型,因此如果有接口需要手动返回特殊格式,直接返回Result对象即可,不会被重复包装。
六、总结
- 这套框架是Spring项目的基础标配,大家完全可以掌握并落地到实际项目中,它能帮你规避很多后续的对接和维护问题。
- 进阶扩展:可以在
Result类中添加traceId(链路追踪ID),配合SkyWalking、Zipkin等链路追踪工具,提升分布式项目的问题排查效率。 - 安全优化:对于系统异常,返回给前端的是友好提示,而后端日志要打印完整堆栈信息,同时避免日志泄露敏感信息(如:用户密码、数据库连接信息)。