Allra后端错误处理标准 allra-error-handling

Allra后端错误处理标准,提供Java后端异常处理、自定义业务异常、全局异常处理器、统一错误响应格式和日志记录策略的完整指南。适用于后端开发、Spring Boot、API设计、错误管理和系统日志。

后端开发 0 次安装 0 次浏览 更新于 3/1/2026

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?