name: allra-error-handling description: Allra 后端错误处理及异常处理标准。用于处理错误、创建自定义异常或实现错误响应时使用。
Allra Backend 错误处理标准
定义 Allra 后端团队的错误处理、异常处理和日志记录标准。
异常类设计
1. 业务异常层次结构
// 顶级业务异常
public abstract class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
protected BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
protected BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public int getStatus() {
return errorCode.getStatus();
}
}
// ErrorCode 枚举 (示例)
public enum ErrorCode {
// 400 Bad Request
INVALID_INPUT_VALUE(400, "E001", "无效的输入值"),
// 401 Unauthorized
UNAUTHORIZED(401, "E101", "需要认证"),
INVALID_TOKEN(401, "E102", "无效的令牌"),
// 403 Forbidden
FORBIDDEN(403, "E201", "没有权限"),
// 404 Not Found
ENTITY_NOT_FOUND(404, "E301", "找不到请求的资源"),
USER_NOT_FOUND(404, "E302", "找不到用户"),
// 409 Conflict
DUPLICATE_RESOURCE(409, "E401", "资源已存在"),
// 500 Internal Server Error
INTERNAL_SERVER_ERROR(500, "E999", "服务器内部发生错误");
private final int status;
private final String code;
private final String message;
ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
// getters...
}
注意: ErrorCode 体系(E001, E101等)和消息语言(韩语/英语)可能因项目而异。
2. 领域特定异常类
// 找不到实体时
public class EntityNotFoundException extends BusinessException {
public EntityNotFoundException(String entityName, Long id) {
super(ErrorCode.ENTITY_NOT_FOUND,
String.format("找不到 %s(id=%d)", entityName, id));
}
}
// 用户相关异常
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(Long userId) {
super(ErrorCode.USER_NOT_FOUND,
String.format("找不到用户(id=%d)", userId));
}
}
// 重复资源异常
public class DuplicateResourceException extends BusinessException {
public DuplicateResourceException(String resourceName, String field, String value) {
super(ErrorCode.DUPLICATE_RESOURCE,
String.format("%s 的 %s=%s 已存在", resourceName, field, value));
}
}
// 认证/授权异常
public class UnauthorizedException extends BusinessException {
public UnauthorizedException() {
super(ErrorCode.UNAUTHORIZED);
}
}
public class ForbiddenException extends BusinessException {
public ForbiddenException(String message) {
super(ErrorCode.FORBIDDEN, message);
}
}
全局异常处理器
@RestControllerAdvice 实现
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常处理
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("BusinessException: code={}, message={}",
e.getErrorCode().getCode(), e.getMessage());
ErrorResponse response = ErrorResponse.of(e.getErrorCode(), e.getMessage());
return ResponseEntity
.status(e.getStatus())
.body(response);
}
// Bean Validation 异常处理
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
log.warn("MethodArgumentNotValidException: {}", e.getMessage());
List<ErrorResponse.FieldError> fieldErrors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue() != null ? error.getRejectedValue().toString() : null,
error.getDefaultMessage()
))
.toList();
ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, fieldErrors);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(response);
}
// 未预期的异常处理
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("发生未预期的异常", e);
ErrorResponse response = ErrorResponse.of(
ErrorCode.INTERNAL_SERVER_ERROR,
"服务器发生错误"
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
错误响应格式 (Allra 标准)
ErrorResponse DTO
public record ErrorResponse(
String code,
String message,
List<FieldError> errors,
LocalDateTime timestamp
) {
public static ErrorResponse of(ErrorCode errorCode) {
return new ErrorResponse(
errorCode.getCode(),
errorCode.getMessage(),
Collections.emptyList(),
LocalDateTime.now()
);
}
public static ErrorResponse of(ErrorCode errorCode, String message) {
return new ErrorResponse(
errorCode.getCode(),
message,
Collections.emptyList(),
LocalDateTime.now()
);
}
public static ErrorResponse of(ErrorCode errorCode, List<FieldError> errors) {
return new ErrorResponse(
errorCode.getCode(),
errorCode.getMessage(),
errors,
LocalDateTime.now()
);
}
public record FieldError(
String field,
String rejectedValue,
String message
) {}
}
注意: 错误响应结构可以根据项目进行定制。重要的是保持一致的格式。
错误响应示例
单一错误:
{
"code": "E302",
"message": "找不到用户(id=123)",
"errors": [],
"timestamp": "2024-12-17T10:30:00"
}
验证错误:
{
"code": "E001",
"message": "无效的输入值",
"errors": [
{
"field": "email",
"rejectedValue": "invalid-email",
"message": "不是有效的电子邮件格式"
}
],
"timestamp": "2024-12-17T10:30:00"
}
服务层中的异常使用
1. 实体查询时的异常处理
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
2. 业务逻辑验证
@Service
public class UserService {
@Transactional
public User createUser(SignUpRequest request) {
// 重复检查
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateResourceException("User", "email", request.email());
}
User user = User.create(request.email(), request.password());
return userRepository.save(user);
}
@Transactional
public void deleteUser(Long id, Long currentUserId) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// 权限检查
if (!user.getId().equals(currentUserId)) {
throw new ForbiddenException("只能删除自己的账户");
}
userRepository.delete(user);
}
}
日志记录策略
1. 日志级别
@Service
@Slf4j
public class UserService {
// DEBUG: 开发时调试信息
log.debug("通过 id 查找用户: {}", id);
// INFO: 正常的业务流程
log.info("用户创建成功: userId={}", user.getId());
// WARN: 业务异常 (预期内的错误)
log.warn("找不到用户: userId={}", id);
// ERROR: 系统异常 (未预期的错误)
log.error("创建用户时发生未预期的错误", e);
}
注意: 日志级别和格式可能因项目的日志策略而异。
2. 日志格式
// ✅ 推荐: 结构化信息
log.info("用户注册完成: userId={}, email={}, signupAt={}",
user.getId(), user.getEmail(), LocalDateTime.now());
log.warn("登录尝试失败: email={}, reason={}",
email, "密码错误");
// ❌ 避免: 简单的字符串连接
log.info("User " + user.getId() + " signed up");
何时使用此技能
此技能在以下情况下会自动应用:
- 创建自定义异常类
- 在服务层抛出异常
- 实现全局异常处理器
- 编写错误响应 DTO
- 编写日志记录代码
检查清单
编写错误处理代码时的检查事项:
- [ ] 业务异常是否继承自 BusinessException?
- [ ] ErrorCode 枚举中是否定义了适当的 HTTP 状态码?
- [ ] 全局异常处理器中是否添加了异常处理?
- [ ] 错误响应是否遵循标准格式?
- [ ] 业务异常是否以 WARN 级别记录日志?
- [ ] 系统异常是否以 ERROR 级别记录日志?
- [ ] 敏感信息(如密码)是否未包含在日志中?
- [ ] 是否使用 orElseThrow 处理 Optional?