Skip to content

异常处理

✨ 开箱即用的全局异常处理:自动捕获并统一返回异常信息,让你专注于业务开发。

  • ✅ 统一响应结构: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等注解A0402Bean Validation校验
参数类型错误A0421如传字符串给数字参数
请求方法错误A0402如用GET访问POST接口
JSON解析失败A0421请求体格式错误
ThirdPartyExceptionB0001第三方服务异常
其他未知异常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 方法
  • 使用框架提供的日志和工具方法

Released under the Apache-2.0 License