异常处理最佳实践代码

为什么需要全局异常处理和通用返回类

全局异常处理的设计目标是提供统一的异常捕获、处理机制,确保在应用中出现异常时能够进行集中管理并返回规范的错误信息。它不仅帮助开发者在不同地方减少重复代码,还提升了系统的可维护性和可扩展性。 以下是全局异常处理的几个优势:

  • 一致性: 保证错误响应的格式和结构一致,无论是在前端还是后端。
  • 集中管理: 所有异常都可以在一个地方处理,减少了分散管理的复杂性。
  • 日志记录: 统一的异常捕获能更好地进行日志记录,方便后期排查问题。
  • 用户体验: 提供了清晰且统一的错误信息,帮助前端进行更好的用户提示。

通用返回类(如 BaseResponse)用于封装请求结果的响应数据,并规范返回结构,确保每个接口的响应一致。 设计通用返回类的原因如下:

  • 统一结构: 所有接口返回的数据都符合统一格式,使前端处理更加简单。
  • 扩展性: 返回类支持链式调用,可以便捷地添加额外字段或修改已有字段。
  • 简化代码: 使用通用返回类可以避免每次接口响应都编写重复代码。

错误码枚举 ErrorCode.java

package com.example.common.exception;

/**
 * 错误码枚举
 * 设计规范:
 * - 0: 成功
 * - 4xxxx: 客户端错误(4开头)
 * - 5xxxx: 服务器错误(5开头)
 * - 1xxxx-3xxxx: 业务错误(按模块分类)
 */
public enum ErrorCode {

    // ============ 成功 ============
    SUCCESS(0, "success", "请求成功", ErrorType.SUCCESS),

    // ============ 系统级错误 (4xxxx-5xxxx) ============
    PARAMS_ERROR(40000, "request params error", "请求参数错误", ErrorType.CLIENT),
    NULL_ERROR(40001, "request data is null", "请求数据为空", ErrorType.CLIENT),
    UNAUTHORIZED(40100, "unauthorized", "未登录或令牌失效", ErrorType.CLIENT),
    FORBIDDEN(40300, "forbidden", "无权限访问", ErrorType.CLIENT),
    NOT_FOUND(40400, "not found", "资源不存在", ErrorType.CLIENT),

    SYSTEM_ERROR(50000, "system error", "系统内部错误", ErrorType.SERVER),
    DATABASE_ERROR(50100, "database error", "数据库错误", ErrorType.SERVER),
    EXTERNAL_SERVICE_ERROR(50200, "external service error", "外部服务调用失败", ErrorType.SERVER),
    CACHE_ERROR(50300, "cache error", "缓存操作失败", ErrorType.SERVER),

    // ============ 用户模块 (1xxxx) ============
    USER_NOT_FOUND(10001, "user not found", "用户不存在", ErrorType.CLIENT),
    USER_ALREADY_EXISTS(10002, "user already exists", "用户已存在", ErrorType.CLIENT),
    PASSWORD_ERROR(10003, "password error", "密码错误", ErrorType.CLIENT),
    EMAIL_ALREADY_EXISTS(10004, "email already exists", "邮箱已存在", ErrorType.CLIENT),
    INVALID_TOKEN(10005, "invalid token", "无效的令牌", ErrorType.CLIENT),

    // ============ 作品模块 (2xxxx) ============
    ARTWORK_NOT_FOUND(20001, "artwork not found", "作品不存在", ErrorType.CLIENT),
    ARTWORK_UPLOAD_FAILED(20002, "artwork upload failed", "作品上传失败", ErrorType.SERVER),
    ARTWORK_PERMISSION_DENIED(20003, "artwork permission denied", "无权操作该作品", ErrorType.CLIENT),

    // ============ 拍卖模块 (3xxxx) ============
    AUCTION_NOT_FOUND(30001, "auction not found", "拍卖不存在", ErrorType.CLIENT),
    BID_PRICE_TOO_LOW(30002, "bid price too low", "出价过低", ErrorType.CLIENT),
    AUCTION_CLOSED(30003, "auction closed", "拍卖已结束", ErrorType.CLIENT);

    private final int code;
    private final String message;
    private final String description;
    private final ErrorType type;

    /**
     * 错误类型枚举
     * CLIENT: 客户端错误(4xxxx),可返回给前端展示
     * SERVER: 服务器错误(5xxxx),需要隐藏敏感信息,返回Trace ID用于追踪
     * SUCCESS: 成功
     */
    public enum ErrorType {
        CLIENT, SERVER, SUCCESS
    }

    ErrorCode(int code, String message, String description, ErrorType type) {
        this.code = code;
        this.message = message;
        this.description = description;
        this.type = type;
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
    public String getDescription() { return description; }
    public ErrorType getType() { return type; }
}

统一响应格式 BaseResponse.java

package com.example.common.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 统一响应格式
 * @param <T> 数据泛型
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 状态码(必填)
     */
    private int code;

    /**
     * 消息(必填,简短提示)
     */
    private String message;

    /**
     * 描述(可选,详细说明)
     */
    private String description;

    /**
     * 响应数据(可选,成功时返回)
     */
    private T data;

    /**
     * 响应时间戳(必填,单位毫秒)
     */
    private long timestamp;

    /**
     * Trace ID(可选,服务器错误时返回,用于问题追踪)
     */
    private String traceId;
}

自定义业务异常 BusinessException.java

package com.example.common.exception;

/**
 * 业务异常
 * 用于表示业务逻辑中的各种异常情况
 * 异常被全局处理器捕获后,会转换成统一的响应格式
 */
public class BusinessException extends RuntimeException {

    private final int code;
    private final String description;
    private final ErrorCode.ErrorType errorType;

    /**
     * 使用错误码构造
     */
    public BusinessException(ErrorCode errorCode) {
        this(errorCode.getMessage(), errorCode.getCode(), errorCode.getDescription(), errorCode.getType());
    }

    /**
     * 使用错误码和自定义描述构造
     */
    public BusinessException(ErrorCode errorCode, String description) {
        this(errorCode.getMessage(), errorCode.getCode(), description, errorCode.getType());
    }

    /**
     * 完整构造(一般不直接使用)
     */
    public BusinessException(String message, int code, String description, ErrorCode.ErrorType errorType) {
        super(message);
        this.code = code;
        this.description = description;
        this.errorType = errorType;
    }

    public int getCode() { return code; }
    public String getDescription() { return description; }
    public ErrorCode.ErrorType getErrorType() { return errorType; }

    @Override
    public String toString() {
        return String.format("BusinessException [code=%d, message=%s, description=%s, type=%s]",
                code, getMessage(), description, errorType);
    }
}

快速异常抛出工具 ThrowUtils.java

package com.canvaschain.common.exception;

import com.canvaschain.common.response.ErrorCode;

/**
 * 异常抛出工具类
 * 简化异常抛出的语法,使代码更简洁
 *
 * 使用示例:
 * ThrowUtils.throwIf(user == null, ErrorCode.USER_NOT_FOUND);
 * ThrowUtils.throwIf(price <= 0, ErrorCode.BID_PRICE_TOO_LOW, "价格必须大于0");
 */
public class ThrowUtils {

    /**
     * 条件成立则抛异常(基础方法)
     */
    public static void throwIf(boolean condition, RuntimeException exception) {
        if (condition) {
            throw exception;
        }
    }

    /**
     * 条件成立则抛业务异常
     */
    public static void throwIf(boolean condition, ErrorCode errorCode) {
        if (condition) {
            throw new BusinessException(errorCode);
        }
    }

    /**
     * 条件成立则抛业务异常(支持自定义描述)
     */
    public static void throwIf(boolean condition, ErrorCode errorCode, String description) {
        if (condition) {
            throw new BusinessException(errorCode, description);
        }
    }

    /**
     * 对象为null时抛异常
     */
    public static <T> T throwIfNull(T obj, ErrorCode errorCode) {
        if (obj == null) {
            throw new BusinessException(errorCode);
        }
        return obj;
    }

    /**
     * 对象为null时抛异常(支持自定义描述)
     */
    public static <T> T throwIfNull(T obj, ErrorCode errorCode, String description) {
        if (obj == null) {
            throw new BusinessException(errorCode, description);
        }
        return obj;
    }

    /**
     * 字符串为空时抛异常
     */
    public static String throwIfBlank(String str, ErrorCode errorCode) {
        if (str == null || str.trim().isEmpty()) {
            throw new BusinessException(errorCode);
        }
        return str;
    }

    /**
     * 字符串为空时抛异常(支持自定义描述)
     */
    public static String throwIfBlank(String str, ErrorCode errorCode, String description) {
        if (str == null || str.trim().isEmpty()) {
            throw new BusinessException(errorCode, description);
        }
        return str;
    }

    /**
     * 集合为空时抛异常
     */
    public static <T> T throwIfEmpty(T collection, ErrorCode errorCode) {
        if (collection == null || (collection instanceof java.util.Collection && ((java.util.Collection<?>) collection).isEmpty())) {
            throw new BusinessException(errorCode);
        }
        return collection;
    }

    /**
     * 集合为空时抛异常(支持自定义描述)
     */
    public static <T> T throwIfEmpty(T collection, ErrorCode errorCode, String description) {
        if (collection == null || (collection instanceof java.util.Collection && ((java.util.Collection<?>) collection).isEmpty())) {
            throw new BusinessException(errorCode, description);
        }
        return collection;
    }
}

响应构造工具 ResultUtils.java

package com.example.common.response;

import com.example.common.exception.ErrorCode;

/**
 * 响应工具类
 * 提供简洁的API用于构造各种响应对象
 */
public class ResultUtils {

    /**
     * 成功响应(返回数据)
     */
    public static <T> BaseResponse<T> success(T data) {
        return BaseResponse.<T>builder()
                .code(ErrorCode.SUCCESS.getCode())
                .message(ErrorCode.SUCCESS.getMessage())
                .description(null)
                .data(data)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    /**
     * 成功响应(无数据)
     */
    public static <T> BaseResponse<T> success() {
        return success(null);
    }

    /**
     * 失败响应(使用错误码)
     */
    public static <T> BaseResponse<T> error(ErrorCode errorCode) {
        return BaseResponse.<T>builder()
                .code(errorCode.getCode())
                .message(errorCode.getMessage())
                .description(errorCode.getDescription())
                .timestamp(System.currentTimeMillis())
                .build();
    }

    /**
     * 失败响应(使用错误码和自定义描述)
     */
    public static <T> BaseResponse<T> error(ErrorCode errorCode, String description) {
        return BaseResponse.<T>builder()
                .code(errorCode.getCode())
                .message(errorCode.getMessage())
                .description(description)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    /**
     * 失败响应(自定义code和message)
     */
    public static <T> BaseResponse<T> error(int code, String message, String description) {
        return BaseResponse.<T>builder()
                .code(code)
                .message(message)
                .description(description)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    /**
     * 服务器错误响应(包含Trace ID)
     */
    public static <T> BaseResponse<T> serverError(String traceId, String description) {
        return BaseResponse.<T>builder()
                .code(ErrorCode.SYSTEM_ERROR.getCode())
                .message("System error, please contact support")
                .description("系统错误,请联系管理员")
                .timestamp(System.currentTimeMillis())
                .traceId(traceId)
                .build();
    }
}

全局异常处理器 GlobalExceptionHandler.java

package com.zwnsyw.zwwwspringbootbasetemplate.exception;

import com.zwnsyw.zwwwspringbootbasetemplate.common.response.BaseResponse;
import com.zwnsyw.zwwwspringbootbasetemplate.common.response.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 全局异常处理器
 * <p>
 * 功能:
 * 1. 捕获所有异常,转换为统一响应格式
 * 2. 记录详细的堆栈信息,精确定位代码行号
 * 3. 区分客户端错误和服务端错误,进行不同处理
 * 4. 服务端错误生成 TraceId 用于问题追踪
 * </p>
 *
 * @author your-name
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // ==================== 业务异常处理 ====================

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public BaseResponse<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
        logBusinessException(e, request);
        return handleByErrorType(e.getErrorType(), e.getCode(), e.getMessage(), e.getDescription());
    }

    /**
     * 处理运行时异常
     * <p>
     * 特殊处理:检查是否是包装的 BusinessException
     * 场景:CompletableFuture 等异步操作会将 BusinessException 包装成 RuntimeException
     * </p>
     */
    @ExceptionHandler(RuntimeException.class)
    public BaseResponse<?> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
        // 检查是否是包装的 BusinessException
        Throwable cause = e.getCause();
        if (cause instanceof BusinessException) {
            return handleBusinessException((BusinessException) cause, request);
        }

        // 普通运行时异常按系统异常处理
        String traceId = UUID.randomUUID().toString();
        logUnexpectedException(e, request, traceId);
        return ResultUtils.serverError(traceId, "系统异常");
    }

    // ==================== 参数校验异常处理 ====================

    /**
     * 处理参数验证异常(@RequestBody + @Valid)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public BaseResponse<?> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
        String fieldErrors = extractFieldErrors(e.getBindingResult());

        log.warn("Parameter validation failed at [{}] {} - Errors: {}",
                request.getMethod(),
                request.getRequestURI(),
                fieldErrors);

        return ResultUtils.error(ErrorCode.PARAMS_ERROR, fieldErrors);
    }

    /**
     * 处理参数绑定异常(@ModelAttribute / 表单提交)
     */
    @ExceptionHandler(BindException.class)
    public BaseResponse<?> handleBindException(BindException e, HttpServletRequest request) {
        String fieldErrors = extractFieldErrors(e.getBindingResult());

        log.warn("Parameter binding failed at [{}] {} - Errors: {}",
                request.getMethod(),
                request.getRequestURI(),
                fieldErrors);

        return ResultUtils.error(ErrorCode.PARAMS_ERROR, fieldErrors);
    }

    /**
     * 处理非法参数异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public BaseResponse<?> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
        logDetailedException(e, request, "warn");
        return ResultUtils.error(ErrorCode.PARAMS_ERROR, e.getMessage());
    }

    // ==================== 兜底异常处理 ====================

    /**
     * 处理所有未捕获的异常(兜底)
     */
    @ExceptionHandler(Exception.class)
    public BaseResponse<?> handleException(Exception e, HttpServletRequest request) {
        String traceId = UUID.randomUUID().toString();
        logUnexpectedException(e, request, traceId);
        return ResultUtils.serverError(traceId, "系统异常");
    }

    // ==================== 响应生成 ====================

    /**
     * 根据错误类型生成不同响应
     * <p>
     * CLIENT 错误:返回详细信息供前端展示
     * SERVER 错误:隐藏敏感信息,返回 TraceId 用于追踪
     * </p>
     */
    private BaseResponse<?> handleByErrorType(ErrorCode.ErrorType errorType, int code, String message, String description) {
        switch (errorType) {
            case CLIENT:
                return ResultUtils.error(code, message, description);
            case SERVER:
                String traceId = UUID.randomUUID().toString();
                log.error("Server error occurred, TraceId: {}, Description: {}", traceId, description);
                return ResultUtils.serverError(traceId, description);
            default:
                return ResultUtils.error(code, message, description);
        }
    }

    // ==================== 日志记录 ====================

    /**
     * 记录业务异常日志
     */
    private void logBusinessException(BusinessException e, HttpServletRequest request) {
        StackTraceElement element = getThrowingElement(e);

        // 客户端错误用 warn,服务端错误用 error
        if (e.getErrorType() == ErrorCode.ErrorType.CLIENT) {
            log.warn("Business exception at [{}] {} - Code: {}, Message: {}, Location: {}:{}#{}()",
                    request.getMethod(),
                    request.getRequestURI(),
                    e.getCode(),
                    e.getMessage(),
                    element.getClassName(),
                    element.getLineNumber(),
                    element.getMethodName());
        } else {
            log.error("Business exception at [{}] {} - Code: {}, Message: {}, Location: {}:{}#{}()",
                    request.getMethod(),
                    request.getRequestURI(),
                    e.getCode(),
                    e.getMessage(),
                    element.getClassName(),
                    element.getLineNumber(),
                    element.getMethodName(),
                    e);
        }
    }

    /**
     * 记录详细异常日志(带堆栈定位)
     */
    private void logDetailedException(Exception e, HttpServletRequest request, String level) {
        StackTraceElement element = getThrowingElement(e);

        String logMessage = String.format("Exception at [%s] %s - Type: %s, Message: %s, Location: %s:%d#%s(), IP: %s",
                request.getMethod(),
                request.getRequestURI(),
                e.getClass().getSimpleName(),
                e.getMessage(),
                element.getClassName(),
                element.getLineNumber(),
                element.getMethodName(),
                getClientIp(request));

        if ("warn".equals(level)) {
            log.warn(logMessage);
        } else {
            log.error(logMessage, e);
        }
    }

    /**
     * 记录未预期异常的详细日志(含完整堆栈)
     */
    private void logUnexpectedException(Exception e, HttpServletRequest request, String traceId) {
        StackTraceElement element = getThrowingElement(e);

        log.error("Unexpected exception [TraceId: {}] at [{}] {} - Type: {}, Message: {}, Location: {}:{}#{}(), IP: {}",
                traceId,
                request.getMethod(),
                request.getRequestURI(),
                e.getClass().getSimpleName(),
                e.getMessage(),
                element.getClassName(),
                element.getLineNumber(),
                element.getMethodName(),
                getClientIp(request),
                e);  // 最后一个参数打印完整堆栈
    }

    // ==================== 工具方法 ====================

    /**
     * 提取字段验证错误信息
     */
    private String extractFieldErrors(BindingResult bindingResult) {
        return bindingResult.getFieldErrors().stream()
                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
                .collect(Collectors.joining(" | "));
    }

    /**
     * 获取异常的最相关堆栈信息
     * <p>
     * 过滤规则:
     * 1. 排除 ThrowUtils 工具类(因为异常是从工具类抛出的,需要找到真正调用处)
     * 2. 排除 Spring 框架类
     * 3. 排除 Java 反射相关类
     * 4. 排除动态代理类
     * </p>
     */
    private StackTraceElement getThrowingElement(Throwable e) {
        StackTraceElement[] stackTrace = e.getStackTrace();

        for (StackTraceElement element : stackTrace) {
            String className = element.getClassName();

            // 跳过需要过滤的类
            if (shouldSkipClass(className)) {
                continue;
            }

            return element;
        }

        // 如果全部被过滤,返回第一个或默认值
        return stackTrace.length > 0 ? stackTrace[0] :
                new StackTraceElement("Unknown", "unknown", "Unknown.java", -1);
    }

    /**
     * 判断是否应该跳过该类
     */
    private boolean shouldSkipClass(String className) {
        return className.contains(".ThrowUtils") ||                  // 工具类
                className.contains("org.springframework") ||          // Spring 框架
                className.contains("java.lang.reflect") ||            // 反射
                className.contains("sun.reflect") ||                  // Sun 反射
                className.contains("$Proxy") ||                       // JDK 代理
                className.contains("$$EnhancerBySpringCGLIB$$") ||   // CGLIB 代理
                className.contains("$$FastClassBySpringCGLIB$$");    // CGLIB FastClass
    }

    /**
     * 获取客户端真实 IP 地址
     * <p>
     * 优先级:X-Forwarded-For > X-Real-IP > Proxy-Client-IP > WL-Proxy-Client-IP > RemoteAddr
     * </p>
     */
    private String getClientIp(HttpServletRequest request) {
        String[] headerNames = {
                "X-Forwarded-For",
                "X-Real-IP",
                "Proxy-Client-IP",
                "WL-Proxy-Client-IP",
                "HTTP_CLIENT_IP",
                "HTTP_X_FORWARDED_FOR"
        };

        for (String header : headerNames) {
            String ip = request.getHeader(header);
            if (isValidIp(ip)) {
                // X-Forwarded-For 可能包含多个 IP,取第一个
                return ip.split(",")[0].trim();
            }
        }

        return request.getRemoteAddr();
    }

    /**
     * 判断 IP 是否有效
     */
    private boolean isValidIp(String ip) {
        return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
    }
}

使用示例

ServiceImpl使用示例

package com.example.user.service.impl;

import com.example.common.exception.ErrorCode;
import com.example.common.exception.ThrowUtils;
import com.example.user.entity.User;
import com.example.user.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl {

    @Autowired
    private UserMapper userMapper;

    /**
     * 示例1:基础使用
     */
    public User getUserById(Long userId) {
        // 参数校验
        ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR);

        // 查询用户
        User user = userMapper.selectById(userId);

        // 业务检查
        ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);

        return user;
    }

    /**
     * 示例2:创建用户(用户名冲突检查)
     */
    public User createUser(String username, String password, String email) {
        // 参数校验
        ThrowUtils.throwIfBlank(username, ErrorCode.PARAMS_ERROR);
        ThrowUtils.throwIfBlank(password, ErrorCode.PARAMS_ERROR);
        ThrowUtils.throwIfBlank(email, ErrorCode.PARAMS_ERROR);

        // 业务校验:用户名重复
        User existing = userMapper.selectByUsername(username);
        ThrowUtils.throwIf(existing != null, ErrorCode.USER_ALREADY_EXISTS,
                "Username '" + username + "' already exists");

        // 创建用户
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        user.setEmail(email);
        userMapper.insert(user);

        return user;
    }

    /**
     * 示例3:复杂业务逻辑
     */
    public void updateUserEmail(Long userId, String newEmail) {
        User user = getUserById(userId);

        // 检查邮箱是否已被占用
        User existing = userMapper.selectByEmail(newEmail);
        ThrowUtils.throwIf(existing != null && !existing.getId().equals(userId),
                ErrorCode.EMAIL_ALREADY_EXISTS,
                "Email '" + newEmail + "' is already in use");

        user.setEmail(newEmail);
        userMapper.updateById(user);
    }
}

Controller 使用示例

// ==================== 8.  ====================
package com.example.user.controller;

import com.example.common.response.BaseResponse;
import com.example.common.response.ResultUtils;
import com.example.user.dto.UserDTO;
import com.example.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取用户信息
     * 异常处理:
     * - USER_NOT_FOUND → 400 客户端错误
     * - 系统异常 → 500 服务器错误 + Trace ID
     */
    @GetMapping("/{userId}")
    public BaseResponse<UserDTO> getUser(@PathVariable Long userId) {
        UserDTO user = userService.getUserById(userId);
        return ResultUtils.success(user);
    }

    /**
     * 创建用户
     * 自动处理参数验证异常
     */
    @PostMapping
    public BaseResponse<UserDTO> createUser(@RequestBody UserDTO dto) {
        UserDTO user = userService.createUser(dto);
        return ResultUtils.success(user);
    }

    /**
     * 更新用户邮箱
     */
    @PutMapping("/{userId}/email")
    public BaseResponse<Void> updateEmail(@PathVariable Long userId,
                                          @RequestParam String newEmail) {
        userService.updateUserEmail(userId, newEmail);
        return ResultUtils.success();
    }
}

项目配置建议

pom.xml 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

application.yml 配置:

server:
  servlet:
    encoding:
      charset: UTF-8
      force: true

logging:
  level:
    root: INFO
    com.example: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

项目分区导航:⬅️ 01-异常处理最佳实践指南 | 02-异常处理最佳实践代码 | ➡️ 03-Spring Boot 多模块自动装配