leniu-error-handler
SKILL.md
leniu-yunshitang-core 异常处理指南
本文档专注于 leniu-tengyun-core 项目的 Java 后端异常处理规范。
快速索引
| 场景 | 推荐方式 |
|---|---|
| 业务异常 | throw new LeException("msg") |
| 带参数异常 | throw new LeException("用户 {} 不存在", userId) |
| 参数校验 | @Validated(InsertGroup.class) |
| 日志记录 | log.error("msg: {}", e.getMessage(), e) |
| 国际化消息 | throw new LeException(I18n.getMessage("key")) |
1. 业务异常 - LeException
基本用法
import com.pig4cloud.pigx.common.core.exception.LeException;
// ✅ 基本用法:抛出业务异常
throw new LeException("用户不存在");
// ✅ 带占位符(支持 {} 占位符)
throw new LeException("用户 {} 不存在", userId);
throw new LeException("订单 {} 状态 {} 无法支付", orderId, status);
// ✅ 条件抛出(手动检查)
if (ObjectUtil.isNull(user)) {
throw new LeException("用户不存在");
}
// ✅ 参数校验
if (StrUtil.isBlank(bo.getName())) {
throw new LeException("名称不能为空");
}
// ✅ 集合判空
if (CollUtil.isEmpty(list)) {
throw new LeException("列表不能为空");
}
2. 国际化 - I18n
国际化消息使用
import net.xnzn.core.common.i18n.I18n;
// 获取国际化消息
String message = I18n.getMessage("user.not.exists");
// 带参数的国际化消息
String message = I18n.getMessage("user.password.retry.limit.exceed", maxRetryCount);
// 在异常中使用
throw new LeException(I18n.getMessage("user.not.exists"));
throw new LeException(I18n.getMessage("order.status.invalid", orderStatus));
// 带占位符的国际化
throw new LeException(I18n.getMessage("user.not.found", userId));
3. 参数校验
使用 @Validated 自动校验
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
// Controller 层校验
@PostMapping("/add")
public Long add(@Validated(InsertGroup.class) @RequestBody LeRequest<XxxDTO> request) {
// 参数校验失败会自动抛出异常
return xxxService.add(request.getContent());
}
@PostMapping("/update")
public void update(@Validated(UpdateGroup.class) @RequestBody LeRequest<XxxDTO> request) {
xxxService.update(request.getContent());
}
DTO 类校验注解
@Data
@ApiModel("XXX DTO")
public class XxxDTO implements Serializable {
@ApiModelProperty(value = "主键ID")
@NotNull(message = "主键ID不能为空", groups = {UpdateGroup.class})
private Long id;
@ApiModelProperty(value = "名称", required = true)
@NotBlank(message = "名称不能为空", groups = {InsertGroup.class, UpdateGroup.class})
@Size(max = 100, message = "名称长度不能超过100个字符")
private String name;
@ApiModelProperty(value = "邮箱")
@Email(message = "邮箱格式不正确")
private String email;
@ApiModelProperty(value = "手机号")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@ApiModelProperty(value = "数量")
@Min(value = 0, message = "数量不能小于0")
@Max(value = 9999, message = "数量不能大于9999")
private Integer count;
@ApiModelProperty(value = "开始时间", required = true)
@NotNull(message = "开始时间不能为空", groups = {InsertGroup.class, UpdateGroup.class})
private Date startTime;
}
分组校验定义
// InsertGroup.java
public interface InsertGroup {}
// UpdateGroup.java
public interface UpdateGroup {}
4. 日志规范
日志级别
| 级别 | 使用场景 | 示例 |
|---|---|---|
| ERROR | 系统错误、业务异常 | 数据库连接失败、第三方接口超时 |
| WARN | 警告信息、潜在问题 | 缓存未命中、重试操作 |
| INFO | 重要业务流程、操作记录 | 用户登录、订单创建 |
| DEBUG | 开发调试信息 | 方法入参、中间变量 |
日志最佳实践
import lombok.extern.slf4j.Slf4j;
import cn.hutool.core.util.ObjectUtil;
@Slf4j
@Service
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
// ✅ 好的:使用占位符(性能更好)
@Override
public Long add(XxxDTO dto) {
log.info("开始新增XXX,名称: {}", dto.getName());
// 业务逻辑
XxxEntity entity = BeanUtil.copyProperties(dto, XxxEntity.class);
xxxMapper.insert(entity);
log.info("新增XXX成功,ID: {}", entity.getId());
return entity.getId();
}
// ❌ 不好:字符串拼接(每次都会拼接,即使日志级别不输出)
@Override
public Long addBad(XxxDTO dto) {
log.info("开始新增XXX,名称: " + dto.getName()); // ❌
// ...
}
// ✅ 好的:异常日志带堆栈(第三个参数传异常对象)
@Override
public XxxVO getById(Long id) {
try {
XxxEntity entity = xxxMapper.selectById(id);
if (ObjectUtil.isNull(entity)) {
throw new LeException("记录不存在");
}
return BeanUtil.copyProperties(entity, XxxVO.class);
} catch (Exception e) {
log.error("查询失败: {}", e.getMessage(), e); // ✅
throw new LeException("查询失败");
}
}
// ❌ 不好:只记录消息,丢失堆栈
@Override
public void bad() {
try {
// ...
} catch (Exception e) {
log.error("处理失败: {}", e.getMessage()); // ❌
throw new LeException("处理失败");
}
}
// ✅ 好的:判断日志级别(避免不必要的序列化开销)
@Override
public void debugMethod(XxxDTO dto) {
if (log.isDebugEnabled()) {
log.debug("详细数据: {}", JacksonUtil.writeValueAsString(dto));
}
// ...
}
}
5. Service 层完整示例
import com.pig4cloud.pigx.common.core.exception.LeException;
import com.pig4cloud.pigx.common.core.util.LeRequest;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import net.xnzn.core.common.i18n.I18n;
import net.xnzn.core.xxx.dto.XxxDTO;
import net.xnzn.core.xxx.mapper.XxxMapper;
import net.xnzn.core.xxx.model.XxxEntity;
import net.xnzn.core.xxx.vo.XxxVO;
import javax.annotation.Resource;
import java.util.List;
/**
* XXX 服务实现
*/
@Slf4j
@Service
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
@Override
public Long add(XxxDTO dto) {
log.info("开始新增XXX,类型: {}", dto.getXXXType());
// 参数校验
if (StrUtil.isBlank(dto.getName())) {
throw new LeException(I18n.getMessage("name.required"));
}
// 业务校验
if (dto.getStartTime().after(dto.getEndTime())) {
throw new LeException(I18n.getMessage("time.invalid"));
}
// 转换并保存
XxxEntity entity = BeanUtil.copyProperties(dto, XxxEntity.class);
entity.setDelFlag(2); // 正常状态
xxxMapper.insert(entity);
log.info("新增XXX成功,ID: {}", entity.getId());
return entity.getId();
}
@Override
public void update(XxxDTO dto) {
if (ObjectUtil.isNull(dto.getId())) {
throw new LeException(I18n.getMessage("id.required"));
}
XxxEntity exist = xxxMapper.selectById(dto.getId());
if (ObjectUtil.isNull(exist)) {
throw new LeException(I18n.getMessage("record.not.exists"));
}
XxxEntity entity = BeanUtil.copyProperties(dto, XxxEntity.class);
xxxMapper.updateById(entity);
log.info("更新XXX成功,ID: {}", dto.getId());
}
@Override
public void delete(Long id) {
if (ObjectUtil.isNull(id)) {
throw new LeException(I18n.getMessage("id.required"));
}
int rows = xxxMapper.deleteById(id);
if (rows == 0) {
throw new LeException(I18n.getMessage("delete.failed"));
}
log.info("删除XXX成功,ID: {}", id);
}
}
6. 用户友好提示
错误提示规范
// ✅ 好的:用户友好提示
throw new LeException("订单已发货,无法取消");
throw new LeException("库存不足,请减少购买数量");
throw new LeException("验证码已过期,请重新获取");
throw new LeException("该用户名已被注册,请换一个试试");
// ❌ 不好:技术术语
throw new LeException("order.status.invalid");
throw new LeException("NullPointerException at line 123");
throw new LeException("数据库连接失败");
throw new LeException("Duplicate entry for key 'uk_username'");
7. 常见错误对比
❌ 错误写法
// 错误 1: 使用 RuoYi 的 ServiceException
throw new ServiceException("用户不存在"); // ❌ 应该用 LeException
// 错误 2: 使用 RuoYi 的 MessageUtils
throw new ServiceException(MessageUtils.message("user.not.exists")); // ❌ 应该用 I18n.getMessage()
// 错误 3: 使用 javax.validation(JDK 21 应用 jakarta.validation)
import javax.validation.Valid; // ❌ 错误
// 错误 4: 使用 RuoYi 的 AddGroup/EditGroup
@Validated(AddGroup.class) // ❌ 应该用 InsertGroup.class
// 错误 5: 使用 RuoYi 的 StringUtils
StringUtils.isBlank(str); // ❌ 应该用 StrUtil.isBlank()
// 错误 6: 使用 RuoYi 的 CollUtil
CollUtil.isEmpty(list); // ❌ 应该用 CollUtil.isEmpty()(Hutool)
// 错误 7: 日志不带堆栈
log.error("处理失败: {}", e.getMessage()); // ❌
// 错误 8: 字符串拼接日志
log.info("处理用户: " + userId); // ❌
✅ 正确写法
// 正确 1: 使用 leniu 的 LeException
throw new LeException("用户不存在"); // ✅
// 正确 2: 使用 leniu 的 I18n
throw new LeException(I18n.getMessage("user.not.exists")); // ✅
// 正确 3: 使用 Jakarta Validation
import jakarta.validation.Valid; // ✅
// 正确 4: 使用 leniu 的 InsertGroup/UpdateGroup
@Validated(InsertGroup.class) // ✅
// 正确 5: 使用 Hutool 的 StrUtil
StrUtil.isBlank(str); // ✅
// 正确 6: 使用 Hutool 的 CollUtil
CollUtil.isEmpty(list); // ✅
// 正确 7: 日志带堆栈
log.error("处理失败: {}", e.getMessage(), e); // ✅
// 正确 8: 日志使用占位符
log.info("处理用户: {}", userId); // ✅
8. 工具类选择
| 场景 | RuoYi-Vue-Plus | leniu-tengyun-core |
|---|---|---|
| 异常类 | ServiceException |
LeException |
| 国际化 | MessageUtils.message(key) |
I18n.getMessage(key) |
| 静态工厂 | 无(必须 new) |
无(必须 new) |
| 对象判空 | ObjectUtil.isNull() |
ObjectUtil.isNull() |
| 集合判空 | CollUtil.isEmpty() |
CollUtil.isEmpty() |
| 字符串判空 | StringUtils.isBlank() |
StrUtil.isBlank() |
| 日志框架 | @Slf4j |
@Slf4j |
9. 检查清单
使用异常处理前必须检查:
- 业务异常使用
new LeException()? - 条件判断使用
if + ObjectUtil.isNull()? - 参数校验使用
@Validated(InsertGroup.class)? - 异常消息是否使用国际化(
I18n.getMessage())? - 日志记录异常堆栈(
log.error("msg: {}", e.getMessage(), e))? - 日志使用占位符
{},不使用字符串拼接? - 重要操作记录 INFO 日志?
- 错误提示使用用户友好语言?
- 是否使用 Jakarta Validation?
- 分组是否正确(InsertGroup/UpdateGroup)?
- 是否使用正确的工具类(StrUtil/CollUtil)?
10. 快速对照表
| ❌ 错误写法 | ✅ 正确写法 |
|---|---|
throw new ServiceException("msg") |
throw new LeException("msg") |
MessageUtils.message("key") |
I18n.getMessage("key") |
import javax.validation.Valid |
import jakarta.validation.Valid |
@Validated(AddGroup.class) |
@Validated(InsertGroup.class) |
StringUtils.isBlank(str) |
StrUtil.isBlank(str) |
log.error("失败: " + e.getMessage()) |
log.error("失败: {}", e.getMessage(), e) |
log.info("用户: " + userId) |
log.info("用户: {}", userId) |
throw new LeException("DB error") |
throw new LeException("数据保存失败,请重试") |
参考代码位置
| 类型 | 路径 |
|---|---|
| Service 示例 | core-attendance/.../service/impl/AttendanceLeaveInfoServiceImpl.java |
项目路径:/Users/xujiajun/Developer/gongsi_proj/leniu-api/leniu-tengyun-core
Weekly Installs
3
Repository
xu-cell/ai-engi…ing-initGitHub Stars
9
First Seen
9 days ago
Security Audits
Installed on
gemini-cli3
github-copilot3
codex3
kimi-cli3
cursor3
amp3