异常处理
✨ 开箱即用的全局异常处理:自动捕获并统一返回异常信息,让你专注于业务开发。
- ✅ 统一响应结构:code、message、userTip、timestamp
- 🧩 标准错误码体系:A(用户端)/ B(系统)/ C(第三方)
- 🔗 无缝整合:Bean Validation、Spring MVC 参数绑定、Jackson 反序列化
- 🛡️ 详细日志:请求 URI、方法、客户端 IP、字段错误、原始错误信息
快速开始
引入 Starter 后默认启用全局异常处理,也可在应用主类显式开启:
java
import host.springboot.framework.context.advice.annotation.EnableGlobalControllerAdvice;
@EnableGlobalControllerAdvice
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}💡 提示:无需手动 try/catch 绝大多数异常,直接抛出即可由框架统一处理并返回友好提示。
基础用法
业务异常
java
@Service
public class UserService {
public User login(String username, String password) {
User user = userMapper.findByUsername(username);
// 抛出业务异常 - 框架会自动处理并返回友好提示
if (user == null) {
throw new ApplicationException(
ErrorCodeEnum.USER_ACCOUNT_NOT_EXIST, // 错误码
"该用户不存在,请先注册" // 用户提示
);
}
if (!password.equals(user.getPassword())) {
throw new ApplicationException(
ErrorCodeEnum.USER_PASSWORD_VERIFY_FAILED,
"密码错误,请重新输入"
);
}
return user;
}
}返回给前端的JSON:
json
{
"code": "A0210",
"message": "用户密码错误",
"userTip": "密码错误,请重新输入",
"timestamp": 1701234567890
}参数校验异常
java
// 1. 实体类添加校验注解
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Length(min = 6, max = 20, message = "密码长度6-20位")
private String password;
}
// 2. Controller 使用 @Valid 注解
@PostMapping("/register")
public BaseVO register(@Valid @RequestBody UserRegisterRequest request) {
// 校验失败自动拦截,无需手动判断
userService.register(request);
return R.ok("注册成功");
}校验失败时自动返回:
json
{
"code": "A0402",
"message": "无效的用户输入",
"userTip": "用户名不能为空",
"timestamp": 1701234567890
}核心异常类
ApplicationException - 业务异常
使用场景: 业务逻辑错误,如用户不存在、余额不足、状态异常等。
java
// 方式1: 使用框架提供的错误码(推荐)
throw new ApplicationException(
ErrorCodeEnum.INSUFFICIENT_BALANCE, // 标准错误码
"账户余额不足,请充值" // 用户友好提示
);
// 方式2: 自定义错误码
throw new ApplicationException(
"ORDER_001", // 自定义错误码
"订单状态异常", // 开发人员看的消息
"该订单无法取消" // 用户看的提示
);ThirdPartyException - 第三方服务异常
使用场景: 调用外部服务失败,如支付接口、短信发送、OSS上传等。
java
try {
AlipayResponse response = alipayClient.pay(request);
if (!response.isSuccess()) {
throw new ThirdPartyException(
response.getCode(), // 支付宝返回的错误码
response.getMsg(), // 支付宝的错误描述
"支付失败,请稍后重试" // 给用户看的提示
);
}
} catch (AlipayApiException e) {
throw new ThirdPartyException("ALIPAY_ERROR", e.getMessage());
}MybatisServiceException - 数据库操作异常
使用场景: 数据库操作失败,如记录不存在、批量操作失败等。
java
@Service
public class ProductService extends BaseServiceImpl<ProductMapper, Product> {
public Product getProduct(Long id) {
// 自动校验 ID 并抛出异常(推荐)
return this.getByIdValidate(id);
}
public void updateProduct(Product product) {
boolean success = this.updateById(product);
// 检查操作结果
this.checkOperation(success, "商品更新失败");
}
}自定义业务异常
步骤1: 定义错误码枚举
java
@AllArgsConstructor
public enum OrderErrorEnum implements BaseEnum<String> {
ORDER_NOT_FOUND("ORD001", "订单不存在"),
ORDER_EXPIRED("ORD002", "订单已过期"),
ORDER_PAID("ORD003", "订单已支付");
private final String value;
private final String reasonPhrase;
@Override
public String getValue() { return this.value; }
@Override
public String getReasonPhrase() { return this.reasonPhrase; }
}步骤2: 创建异常类(可选)
java
public class OrderException extends ApplicationException {
public OrderException(OrderErrorEnum errorEnum, String userTip) {
super(errorEnum, userTip);
}
}步骤3: 使用自定义异常
java
@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new OrderException(
OrderErrorEnum.ORDER_NOT_FOUND,
"订单号" + orderId + "不存在"
);
}
if (order.isPaid()) {
throw new OrderException(
OrderErrorEnum.ORDER_PAID,
"订单已支付,无法取消"
);
}
}
}常见场景
场景1: 参数校验
java
// 使用注解自动校验(推荐)
public class CreateOrderRequest {
@NotNull(message = "商品ID不能为空")
private Long productId;
@Min(value = 1, message = "购买数量至少为1")
private Integer quantity;
@NotBlank(message = "收货地址不能为空")
private String address;
}
@PostMapping("/order/create")
public BaseVO createOrder(@Valid @RequestBody CreateOrderRequest request) {
// 校验失败会自动返回错误,无需手动处理
return R.okSingle(orderService.create(request));
}场景2: 业务规则校验
java
@Service
public class OrderService {
public Order create(CreateOrderRequest request) {
// 检查库存
Product product = productService.getById(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new ApplicationException(
ErrorCodeEnum.USER_RESOURCE_ERROR,
"库存不足,仅剩" + product.getStock() + "件"
);
}
// 检查用户余额
BigDecimal totalPrice = product.getPrice().multiply(
new BigDecimal(request.getQuantity())
);
if (user.getBalance().compareTo(totalPrice) < 0) {
throw new ApplicationException(
ErrorCodeEnum.INSUFFICIENT_BALANCE,
"余额不足,请充值"
);
}
// 创建订单...
}
}场景3: 数据库操作
java
@Service
public class UserService extends BaseServiceImpl<UserMapper, User> {
public User getUserById(Long id) {
// 方式1: 使用框架提供的方法(推荐)
return this.getByIdValidate(id); // ID不存在自动抛异常
}
public void deleteUser(Long id) {
// 方式2: 手动校验
User user = this.getById(id);
if (user == null) {
throw new MybatisServiceException(
MybatisServiceErrorEnum.RESULT_IS_NULL,
"用户不存在"
);
}
boolean success = this.removeById(id);
// 检查操作结果
this.checkOperation(success, "删除失败");
}
}场景4: 第三方服务调用
java
@Service
public class SmsService {
public void sendCode(String phone, String code) {
try {
SmsResponse response = smsClient.send(phone, code);
if (!response.isSuccess()) {
throw new ThirdPartyException(
response.getCode(),
response.getMessage(),
"短信发送失败,请稍后重试"
);
}
} catch (SmsException e) {
throw new ThirdPartyException(
"SMS_ERROR",
e.getMessage(),
"短信服务暂时不可用"
);
}
}
}响应格式
所有异常会被框架自动捕获并转换为统一格式:
成功响应
json
{
"code": "00000",
"message": "OK",
"userTip": null,
"timestamp": 1701234567890
}业务异常响应
json
{
"code": "A0210",
"message": "用户密码错误",
"userTip": "密码错误,请重新输入",
"timestamp": 1701234567890
}系统异常响应
json
{
"code": "B0001",
"message": "系统执行出错",
"userTip": "网络开小差了, 请稍后再试 (╯﹏╰)",
"timestamp": 1701234567890
}框架处理的异常类型
| 异常 | 自动处理 | 返回错误码 | 说明 |
|---|---|---|---|
ApplicationException | ✅ | 异常中的errorCode | 业务异常 |
@Valid 校验失败 | ✅ | A0402 | 参数校验失败 |
@NotNull等注解 | ✅ | A0402 | Bean Validation校验 |
| 参数类型错误 | ✅ | A0421 | 如传字符串给数字参数 |
| 请求方法错误 | ✅ | A0402 | 如用GET访问POST接口 |
| JSON解析失败 | ✅ | A0421 | 请求体格式错误 |
ThirdPartyException | ✅ | B0001 | 第三方服务异常 |
| 其他未知异常 | ✅ | B0001 | 兜底处理 |
最佳实践
✅ 推荐做法
java
// 1. 使用框架提供的错误码
throw new ApplicationException(
ErrorCodeEnum.USER_ACCOUNT_NOT_EXIST,
"用户不存在"
);
// 2. 给用户友好的提示信息
throw new ApplicationException(
ErrorCodeEnum.INSUFFICIENT_BALANCE,
"余额不足,当前余额: " + balance + "元"
);
// 3. 使用注解自动校验参数
@NotBlank(message = "用户名不能为空")
private String username;
// 4. 使用框架提供的校验方法
return this.getByIdValidate(id); // 自动抛异常
this.checkOperation(success, "操作失败");❌ 不推荐做法
java
// ❌ 不要返回 null 让调用方判断
if (user == null) return null;
// ❌ 不要抛出原始 RuntimeException
throw new RuntimeException("用户不存在");
// ❌ 不要吞掉异常
try {
// ...
} catch (Exception e) {
// 什么都不做
}
// ❌ 不要硬编码错误信息
throw new ApplicationException("001", "错误", "失败");常见问题
Q: 如何让前端只看到友好提示,不暴露技术细节?
A: 使用 userTip 参数:
java
// 推荐:错误枚举 + 用户提示(开发信息取自枚举)
throw new ApplicationException(
ErrorCodeEnum.SYSTEM_EXECUTION_ERROR,
"网络开小差了"
);java
// 如需自定义开发信息:使用三参构造(错误码、开发信息、用户提示)
throw new ApplicationException(
ErrorCodeEnum.SYSTEM_EXECUTION_ERROR.getValue(),
"数据库连接超时",
"网络开小差了"
);Q: 异常没有被捕获怎么办?
A: 检查是否添加了 krismile-boot3-autoconfigure 依赖,并确保未手动吞掉异常;也可在应用主类上添加注解 @EnableGlobalControllerAdvice 显式开启。
Q: 如何自定义异常处理逻辑?
A: 请查阅 全局异常处理架构 文档,了解如何:
- 继承
GlobalControllerAdvice重写异常处理方法 - 添加自定义的
@ExceptionHandler方法 - 使用框架提供的日志和工具方法
