前言
在前后端分离的项目中,接口返回格式统一、异常处理规范,是非常基础但极其重要的一环。
如果每个 Controller 都自己 try-catch,或者返回格式五花八门,前端处理起来会非常痛苦。比较好的做法是:
- 统一响应结构:
code + message + data - 自定义业务异常:
BusinessException - 全局异常处理:
@RestControllerAdvice - 参数校验异常统一拦截
本文基于 Spring Boot 3 + JDK 17,手把手实现一套通用方案,代码可直接复制使用。
一、项目环境
- JDK 17
- Spring Boot 3.2.x
- Maven
- Lombok(可选,本文使用,简化代码)
二、引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
三、定义统一响应结果
创建 Result<T> 类:
java
package com.example.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(
ResultCode.SUCCESS.getCode(),
ResultCode.SUCCESS.getMessage(),
data
);
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(ResultCode resultCode) {
return new Result<>(
resultCode.getCode(),
resultCode.getMessage(),
null
);
}
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
}
四、定义状态码枚举
创建 ResultCode:
java
package com.example.common;
import lombok.Getter;
@Getter
public enum ResultCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(400, "参数校验失败"),
UNAUTHORIZED(401, "暂未登录或 token 已过期"),
FORBIDDEN(403, "没有相关权限");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
五、自定义业务异常
创建 BusinessException:
java
package com.example.common.exception;
import com.example.common.ResultCode;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final ResultCode resultCode;
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
}
}
业务代码中可以直接抛出:
java
throw new BusinessException(ResultCode.VALIDATE_FAILED);
六、全局异常处理器
创建 GlobalExceptionHandler:
java
package com.example.common.exception;
import com.example.common.Result;
import com.example.common.ResultCode;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getResultCode());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String message = fieldError != null
? fieldError.getDefaultMessage()
: ResultCode.VALIDATE_FAILED.getMessage();
return Result.error(ResultCode.VALIDATE_FAILED.getCode(), message);
}
/**
* 兜底异常处理
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
// 生产环境建议在这里记录日志
return Result.error(ResultCode.FAILED);
}
}
七、测试 Controller
创建 UserDTO:
java
package com.example.controller;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
}
创建 UserVO:
java
package com.example.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserVO {
private Long id;
private String username;
}
创建 UserController:
java
package com.example.controller;
import com.example.common.Result;
import com.example.common.ResultCode;
import com.example.common.exception.BusinessException;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public Result<UserVO> getUser(@PathVariable Long id) {
if (id <= 0) {
throw new BusinessException(ResultCode.VALIDATE_FAILED);
}
return Result.success(new UserVO(id, "张三"));
}
@PostMapping
public Result<Void> create(@RequestBody @Valid UserDTO userDTO) {
// 模拟业务处理
return Result.success();
}
}
八、测试效果
1. 正常请求
http
GET /user/1
返回:
json
{
"code": 200,
"message": "操作成功",
"data": {
"id": 1,
"username": "张三"
}
}
2. 业务异常
http
GET /user/0
返回:
json
{
"code": 400,
"message": "参数校验失败",
"data": null
}
3. 参数校验异常
http
POST /user
Content-Type: application/json
{
"username": ""
}
返回:
json
{
"code": 400,
"message": "用户名不能为空",
"data": null
}
4. 系统异常
如果代码抛出未捕获异常,会统一返回:
json
{
"code": 500,
"message": "操作失败",
"data": null
}
九、总结
这套方案的核心就三点:
- 统一响应结构 :前端只需要判断
code,处理逻辑更简单。 - 自定义业务异常:业务错误主动抛出,代码更清晰。
- 全局异常处理 :所有异常集中拦截,避免到处写
try-catch。
在实际项目中,还可以继续扩展:
- 自定义错误码枚举
- 异常日志记录
- 链路追踪 traceId
- 国际化消息
- 对不同异常返回不同 HTTP 状态码