error-handler

通用模板。如果项目有专属技能(如 leniu-error-handler ),优先使用。

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "error-handler" with this command: npx skills add xu-cell/ai-engineering-init/xu-cell-ai-engineering-init-error-handler

异常处理指南

通用模板。如果项目有专属技能(如 leniu-error-handler ),优先使用。

核心规范

异常分层设计

RuntimeException └── BusinessException # 业务异常基类 ├── NotFoundException # 资源不存在 (404) ├── ForbiddenException # 无权限 (403) ├── BadRequestException # 参数错误 (400) └── ConflictException # 数据冲突 (409)

异常处理原则

  • 业务异常用自定义异常类,不要直接抛 RuntimeException

  • 全局统一捕获,通过 @RestControllerAdvice 处理

  • 区分异常层级:Controller 层不 try-catch(交给全局处理器),Service 层只捕获需要转换的异常

  • 异常信息面向用户:不暴露堆栈、SQL 等技术细节

  • 日志记录完整:异常日志包含完整上下文和堆栈

代码示例

  1. 自定义业务异常

package [你的包名].exception;

import lombok.Getter;

@Getter public class BusinessException extends RuntimeException {

private final int code;

public BusinessException(String message) {
    super(message);
    this.code = 500;
}

public BusinessException(int code, String message) {
    super(message);
    this.code = code;
}

public BusinessException(int code, String message, Throwable cause) {
    super(message, cause);
    this.code = code;
}

}

package [你的包名].exception;

public class NotFoundException extends BusinessException {

public NotFoundException(String message) {
    super(404, message);
}

public static NotFoundException of(String resource, Object id) {
    return new NotFoundException(resource + " 不存在: " + id);
}

}

  1. 全局异常处理器

package [你的包名].handler;

import [你的包名].exception.BusinessException; import [你的包名].exception.NotFoundException; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map; import java.util.stream.Collectors;

@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {

/**
 * 业务异常
 */
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e) {
    log.warn("业务异常: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(Result.fail(e.getCode(), e.getMessage()));
}

/**
 * 资源不存在
 */
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Result<Void>> handleNotFoundException(NotFoundException e) {
    log.warn("资源不存在: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(Result.fail(404, e.getMessage()));
}

/**
 * @RequestBody 参数校验失败
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Map<String, String>>> handleValidationException(
        MethodArgumentNotValidException e) {
    Map<String, String> errors = e.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(
                    FieldError::getField,
                    fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "校验失败",
                    (v1, v2) -> v1
            ));
    log.warn("参数校验失败: {}", errors);
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(Result.fail(400, "参数校验失败"));
}

/**
 * @RequestParam / @PathVariable 校验失败
 */
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Result<Void>> handleConstraintViolation(ConstraintViolationException e) {
    String message = e.getConstraintViolations().stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.joining("; "));
    log.warn("约束校验失败: {}", message);
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(Result.fail(400, message));
}

/**
 * 缺少请求参数
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Result<Void>> handleMissingParam(MissingServletRequestParameterException e) {
    log.warn("缺少请求参数: {}", e.getParameterName());
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(Result.fail(400, "缺少参数: " + e.getParameterName()));
}

/**
 * 兜底:未知异常
 */
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e) {
    log.error("系统异常", e);
    return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(Result.fail(500, "系统繁忙,请稍后重试"));
}

}

  1. 参数校验(jakarta.validation)

package [你的包名].dto;

import jakarta.validation.constraints.*; import lombok.Data;

@Data public class UserCreateDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 32, message = "用户名长度 2-32 位")
private String username;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄最小为 1")
@Max(value = 150, message = "年龄最大为 150")
private Integer age;

@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;

}

Controller 中使用:

@PostMapping public ResponseEntity<Result<Long>> create(@Valid @RequestBody UserCreateDTO dto) { return ResponseEntity.ok(Result.ok(userService.create(dto))); }

  1. 分组校验

public interface CreateGroup {} public interface UpdateGroup {}

@Data public class UserDTO {

@Null(groups = CreateGroup.class, message = "创建时不能指定 ID")
@NotNull(groups = UpdateGroup.class, message = "更新时必须指定 ID")
private Long id;

@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
private String username;

}

// Controller 使用 @PostMapping public Result<Long> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }

@PutMapping("/{id}") public Result<Void> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }

  1. Service 层异常使用

@Service public class UserServiceImpl implements IUserService {

@Override
public UserVO getById(Long id) {
    User user = userMapper.selectById(id);
    if (user == null) {
        throw NotFoundException.of("用户", id);
    }
    // ... 转换为 VO
    return userVO;
}

@Override
public void updateEmail(Long id, String email) {
    // 检查邮箱是否已被使用
    User existing = userMapper.selectByEmail(email);
    if (existing != null &#x26;&#x26; !existing.getId().equals(id)) {
        throw new BusinessException(409, "邮箱已被其他用户使用");
    }
    // ... 更新逻辑
}

}

  1. 日志规范

@Slf4j @Service public class OrderServiceImpl {

// 使用占位符(性能更好)
log.info("创建订单: orderNo={}, amount={}", dto.getOrderNo(), dto.getAmount());

// 异常日志带堆栈(第三个参数传异常对象)
log.error("处理失败: {}", e.getMessage(), e);

// 事务方法:所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderCreateDTO dto) {
    log.info("开始创建订单, orderNo={}", dto.getOrderNo());
    // ... 业务逻辑
    log.info("订单创建成功, id={}", order.getId());
}

}

常见错误

错误 正确做法

抛 RuntimeException("xxx")

使用自定义业务异常类

Controller 里 try-catch 所有异常 交给 @RestControllerAdvice 统一处理

异常信息暴露 SQL / 堆栈 对用户返回友好提示,日志记录完整信息

用 javax.validation 包 JDK 17+ 使用 jakarta.validation

吞掉异常:catch (Exception e) {}

至少记录日志 log.error("...", e)

所有异常都返回 200 状态码 根据异常类型返回对应 HTTP 状态码

用 e.getMessage() 直接返回给用户 第三方异常信息可能包含敏感信息,需要包装

校验逻辑写在 Controller 里 用 @Valid

  • DTO 注解声明式校验

日志用字符串拼接 "失败:" + msg

用占位符 log.error("失败: {}", msg, e)

@Transactional 不指定回滚 加 rollbackFor = Exception.class

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

loki-log-query

No summary provided by upstream source.

Repository SourceNeeds Review
General

scheduled-jobs

No summary provided by upstream source.

Repository SourceNeeds Review
General

leniu-mealtime

No summary provided by upstream source.

Repository SourceNeeds Review