优秀的编程知识分享平台

网站首页 > 技术文章 正文

不再头疼的Spring Boot异常处理:从入门到精通的七步实战(下篇)

nanyue 2025-03-28 19:30:04 技术文章 6 ℃

上篇内容请浏览文章:不再头疼的Spring Boot异常处理:从入门到精通的七步实战(上篇)

5. 实践案例

5.1 完整应用示例

让我们通过一个用户管理系统的例子来展示如何实现完整的异常处理。

5.1.1 定义接口响应包装器和统一错误码

// 统一错误码枚举
package 「包名称,请自行替换」.constants;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {
    // 通用错误
    UNKNOWN_ERROR("COMMON-000", "未知错误", HttpStatus.INTERNAL_SERVER_ERROR),
    PARAM_INVALID("COMMON-001", "参数不合法", HttpStatus.BAD_REQUEST),
    
    // 用户相关错误
    USER_NOT_FOUND("USER-001", "用户不存在", HttpStatus.NOT_FOUND),
    USERNAME_ALREADY_EXISTS("USER-002", "用户名已存在", HttpStatus.CONFLICT),
    EMAIL_ALREADY_EXISTS("USER-003", "邮箱已存在", HttpStatus.CONFLICT),
    PASSWORD_INCORRECT("USER-004", "密码不正确", HttpStatus.BAD_REQUEST);
    
    private final String code;
    private final String message;
    private final HttpStatus status;
    
    ErrorCode(String code, String message, HttpStatus status) {
        this.code = code;
        this.message = message;
        this.status = status;
    }
}

5.1.2 实体和DTO

// 用户实体
package 「包名称,请自行替换」.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    public void preUpdate() {
        this.updatedAt = LocalDateTime.now();
    }
}

// 创建用户DTO
package 「包名称,请自行替换」.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 4, max = 20, message = "用户名长度必须在4-20之间")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
    private String password;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

// 用户返回DTO
package 「包名称,请自行替换」.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
}

5.1.3 仓库和服务层

// 用户仓库
package 「包名称,请自行替换」.repository;

import 「包名称,请自行替换」.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository {
    Optional findByUsername(String username);
    Optional findByEmail(String email);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

// 用户服务接口
package 「包名称,请自行替换」.service;

import 「包名称,请自行替换」.dto.CreateUserRequest;
import 「包名称,请自行替换」.dto.UserResponse;

import java.util.List;

public interface UserService {
    UserResponse createUser(CreateUserRequest request);
    UserResponse getUserById(Long id);
    UserResponse getUserByUsername(String username);
    List getAllUsers();
    void deleteUser(Long id);
}

// 用户服务实现
package 「包名称,请自行替换」.service.impl;

import 「包名称,请自行替换」.constants.ErrorCode;
import 「包名称,请自行替换」.dto.CreateUserRequest;
import 「包名称,请自行替换」.dto.UserResponse;
import 「包名称,请自行替换」.entity.User;
import 「包名称,请自行替换」.exception.BusinessException;
import 「包名称,请自行替换」.exception.ResourceNotFoundException;
import 「包名称,请自行替换」.repository.UserRepository;
import 「包名称,请自行替换」.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    
    @Override
    @Transactional
    public UserResponse createUser(CreateUserRequest request) {
        // 检查用户名是否已存在
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new BusinessException(
                ErrorCode.USERNAME_ALREADY_EXISTS.getMessage(), 
                ErrorCode.USERNAME_ALREADY_EXISTS.getCode()
            );
        }
        
        // 检查邮箱是否已存在
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new BusinessException(
                ErrorCode.EMAIL_ALREADY_EXISTS.getMessage(), 
                ErrorCode.EMAIL_ALREADY_EXISTS.getCode()
            );
        }
        
        // 创建用户
        User user = User.builder()
                .username(request.getUsername())
                .password(request.getPassword()) // 实际应用中需要加密
                .email(request.getEmail())
                .build();
        
        User savedUser = userRepository.save(user);
        log.info("User created successfully: {}", savedUser.getUsername());
        
        return convertToUserResponse(savedUser);
    }

    @Override
    @Transactional(readOnly = true)
    public UserResponse getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException(
                    ErrorCode.USER_NOT_FOUND.getMessage(), 
                    ErrorCode.USER_NOT_FOUND.getCode()
                ));
        
        return convertToUserResponse(user);
    }

    @Override
    @Transactional(readOnly = true)
    public UserResponse getUserByUsername(String username) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new ResourceNotFoundException(
                    ErrorCode.USER_NOT_FOUND.getMessage(), 
                    ErrorCode.USER_NOT_FOUND.getCode()
                ));
        
        return convertToUserResponse(user);
    }

    @Override
    @Transactional(readOnly = true)
    public List getAllUsers() {
        return userRepository.findAll().stream()
                .map(this::convertToUserResponse)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional
    public void deleteUser(Long id) {
        if (!userRepository.existsById(id)) {
            throw new ResourceNotFoundException(
                ErrorCode.USER_NOT_FOUND.getMessage(), 
                ErrorCode.USER_NOT_FOUND.getCode()
            );
        }
        
        userRepository.deleteById(id);
        log.info("User deleted successfully: ID={}", id);
    }
    
    private UserResponse convertToUserResponse(User user) {
        return UserResponse.builder()
                .id(user.getId())
                .username(user.getUsername())
                .email(user.getEmail())
                .createdAt(user.getCreatedAt())
                .build();
    }
}

5.1.4 控制器层

package 「包名称,请自行替换」.controller;

import 「包名称,请自行替换」.dto.CreateUserRequest;
import 「包名称,请自行替换」.dto.UserResponse;
import 「包名称,请自行替换」.model.ApiResponse;
import 「包名称,请自行替换」.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ApiResponse createUser(@Valid @RequestBody CreateUserRequest request) {
        log.info("Creating user with username: {}", request.getUsername());
        UserResponse createdUser = userService.createUser(request);
        return ApiResponse.success(createdUser);
    }
    
    @GetMapping("/{id}")
    public ApiResponse getUserById(@PathVariable Long id) {
        log.info("Fetching user with id: {}", id);
        UserResponse user = userService.getUserById(id);
        return ApiResponse.success(user);
    }
    
    @GetMapping("/username/{username}")
    public ApiResponse getUserByUsername(@PathVariable String username) {
        log.info("Fetching user with username: {}", username);
        UserResponse user = userService.getUserByUsername(username);
        return ApiResponse.success(user);
    }
    
    @GetMapping
    public ApiResponse<List> getAllUsers() {
        log.info("Fetching all users");
        List users = userService.getAllUsers();
        return ApiResponse.success(users);
    }
    
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ApiResponse deleteUser(@PathVariable Long id) {
        log.info("Deleting user with id: {}", id);
        userService.deleteUser(id);
        return ApiResponse.success(null);
    }
}

5.2 测试与验证

让我们创建一个测试类来验证异常处理机制:

package 「包名称,请自行替换」.controller;

import 「包名称,请自行替换」.constants.ErrorCode;
import 「包名称,请自行替换」.dto.CreateUserRequest;
import 「包名称,请自行替换」.exception.BusinessException;
import 「包名称,请自行替换」.exception.ResourceNotFoundException;
import 「包名称,请自行替换」.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private UserService userService;
    
    @Test
    void getUserById_WhenUserNotFound_ShouldReturnNotFound() throws Exception {
        // Given
        Long userId = 1L;
        when(userService.getUserById(userId)).thenThrow(
            new ResourceNotFoundException(
                ErrorCode.USER_NOT_FOUND.getMessage(),
                ErrorCode.USER_NOT_FOUND.getCode()
            )
        );
        
        // When & Then
        mockMvc.perform(get("/api/users/{id}", userId))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.error.code").value(ErrorCode.USER_NOT_FOUND.getCode()))
                .andExpect(jsonPath("$.error.message").value(ErrorCode.USER_NOT_FOUND.getMessage()));
    }
    
    @Test
    void createUser_WhenUsernameExists_ShouldReturnConflict() throws Exception {
        // Given
        CreateUserRequest request = CreateUserRequest.builder()
                .username("testuser")
                .password("password123")
                .email("「邮箱,请自行替换」")
                .build();
                
        when(userService.createUser(any(CreateUserRequest.class))).thenThrow(
            new BusinessException(
                ErrorCode.USERNAME_ALREADY_EXISTS.getMessage(),
                ErrorCode.USERNAME_ALREADY_EXISTS.getCode()
            )
        );
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.error.code").value(ErrorCode.USERNAME_ALREADY_EXISTS.getCode()))
                .andExpect(jsonPath("$.error.message").value(ErrorCode.USERNAME_ALREADY_EXISTS.getMessage()));
    }
    
    @Test
    void createUser_WhenInvalidRequest_ShouldReturnBadRequest() throws Exception {
        // Given
        CreateUserRequest request = CreateUserRequest.builder()
                .username("") // 空用户名,不符合验证要求
                .password("pwd") // 密码太短
                .email("invalid-email") // 无效的邮箱格式
                .build();
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR"))
                .andExpect(jsonPath("$.error.details").isNotEmpty());
    }
}

运行效果

当我们运行测试时,应该能看到以下输出:

测试通过: getUserById_WhenUserNotFound_ShouldReturnNotFound
测试通过: createUser_WhenUsernameExists_ShouldReturnConflict
测试通过: createUser_WhenInvalidRequest_ShouldReturnBadRequest

BUILD SUCCESS

这说明我们的异常处理机制正常工作,能够捕获各种异常并返回统一格式的错误响应。

6. 进阶优化

6.1 异常处理的性能优化

该图展示了四种不同异常处理方式的性能对比。从左到右依次是传统try-catch方式(150ms)、返回错误码方式(85ms)、全局异常处理(120ms)和优化后的异常处理(65ms)。图中可以看出,优化后的异常处理方式在响应时间上有明显优势。

在高并发系统中,异常处理的性能尤为重要。以下是几种优化策略:

1. 避免过度使用异常:异常创建和抛出的成本较高,对于可预见的错误情况,考虑使用返回值或Optional来替代异常

2. 使用异常缓存:对于频繁使用的异常,可以使用静态常量或对象池来避免重复创建

// 异常缓存示例
public class ResourceNotFoundException extends BaseException {
    
    private static final String DEFAULT_ERROR_CODE = "RESOURCE_NOT_FOUND";
    
    // 缓存常用异常实例
    public static final ResourceNotFoundException USER_NOT_FOUND = 
            new ResourceNotFoundException("User not found", "USER_NOT_FOUND");
    public static final ResourceNotFoundException PRODUCT_NOT_FOUND = 
            new ResourceNotFoundException("Product not found", "PRODUCT_NOT_FOUND");
    
    public ResourceNotFoundException(String message, String errorCode) {
        super(message, HttpStatus.NOT_FOUND, errorCode);
    }
}

3. 延迟加载异常详情:仅在需要时再收集堆栈信息,减少异常创建成本

public abstract class BaseException extends RuntimeException {
    
    private final HttpStatus status;
    private final String errorCode;
    private boolean stackTraceEnabled = false;
    
    // 只有在需要时才填充堆栈信息
    @Override
    public synchronized Throwable fillInStackTrace() {
        if (stackTraceEnabled) {
            return super.fillInStackTrace();
        }
        return this;
    }
    
    // 需要堆栈信息时调用此方法
    public Throwable enableStackTrace() {
        this.stackTraceEnabled = true;
        return fillInStackTrace();
    }
}

4. 使用ThreadLocal缓存异常上下文:减少每次创建异常时的上下文收集开销

public class ExceptionContextHolder {
    private static final ThreadLocal<Map> contextHolder = 
            ThreadLocal.withInitial(HashMap::new);
    
    public static void setContext(String key, Object value) {
        contextHolder.get().put(key, value);
    }
    
    public static Object getContext(String key) {
        return contextHolder.get().get(key);
    }
    
    public static void clear() {
        contextHolder.remove();
    }
}

6.2 异常监控与告警

构建异常监控系统是保障应用健康的重要环节:

1. 记录结构化日志:使用MDC(Mapped Diagnostic Context)记录异常上下文信息

@ExceptionHandler(BaseException.class)
public ResponseEntity<ApiResponse> handleBaseException(BaseException ex, WebRequest request) {
    // 添加上下文信息到MDC
    MDC.put("errorCode", ex.getErrorCode());
    MDC.put("path", request.getDescription(false));
    MDC.put("userId", SecurityContextHolder.getContext().getAuthentication().getName());
    
    log.error("BaseException: {}", ex.getMessage(), ex);
    
    // 清理MDC
    MDC.clear();
    
    // 返回响应...
}

2. 集成监控系统:将异常监控与Prometheus、Grafana等监控工具集成

@Component
@RequiredArgsConstructor
public class ExceptionMetrics {
    
    private final MeterRegistry registry;
    
    private final Counter totalExceptions;
    private final Counter businessExceptions;
    private final Counter validationExceptions;
    private final Counter systemExceptions;
    
    @PostConstruct
    public void init() {
        totalExceptions = registry.counter("app.exceptions.total");
        businessExceptions = registry.counter("app.exceptions.business");
        validationExceptions = registry.counter("app.exceptions.validation");
        systemExceptions = registry.counter("app.exceptions.system");
    }
    
    public void recordException(Throwable ex) {
        totalExceptions.increment();
        
        if (ex instanceof BusinessException) {
            businessExceptions.increment();
        } else if (ex instanceof ValidationException) {
            validationExceptions.increment();
        } else {
            systemExceptions.increment();
        }
    }
}

3. 异常聚合与分析:使用ELK(Elasticsearch、Logstash、Kibana)等工具进行异常聚合和分析

6.3 微服务环境下的异常处理

该图描述了微服务架构中的异常处理流程。客户端请求通过API网关到达用户服务和订单服务,各服务内部都包含全局异常处理和错误编码映射组件。当服务产生异常时,异常信息既会返回给API网关,又会发送到集中式日志系统进行异常聚合和告警。API网关将统一格式的响应返回给客户端,确保整个系统的异常处理行为一致。

在微服务架构下,异常处理面临更多挑战:

1. 错误码标准化:不同服务之间需要统一错误码体系

// 错误码接口,所有服务统一实现
public interface ErrorCodeProvider {
    String getCode();
    String getMessage();
    HttpStatus getStatus();
}

// 各服务实现自己的错误码枚举
@Getter
public enum UserErrorCodes implements ErrorCodeProvider {
    USER_NOT_FOUND("USER-001", "用户不存在", HttpStatus.NOT_FOUND),
    // 其他错误码...
    
    private final String code;
    private final String message;
    private final HttpStatus status;
    
    UserErrorCodes(String code, String message, HttpStatus status) {
        this.code = code;
        this.message = message;
        this.status = status;
    }
}

2. 分布式追踪:集成链路追踪,关联不同服务的异常信息

@ExceptionHandler(BaseException.class)
public ResponseEntity<ApiResponse> handleBaseException(BaseException ex, WebRequest request) {
    // 获取链路ID
    String traceId = tracer.currentSpan().context().traceIdString();
    
    ApiResponse.ErrorInfo errorInfo = ApiResponse.ErrorInfo.builder()
            .code(ex.getErrorCode())
            .message(ex.getMessage())
            .path(request.getDescription(false))
            .traceId(traceId) // 添加链路ID
            .build();
    
    // 返回响应...
}

3. 断路器模式:集成失败重试和断路器,防止级联故障

@Service
public class ResilientUserService {
    
    private final UserClient userClient;
    
    @CircuitBreaker(name = "userService", fallbackMethod = "getUserByIdFallback")
    @Retry(name = "userService", fallbackMethod = "getUserByIdFallback")
    public UserResponse getUserById(Long id) {
        return userClient.getUserById(id);
    }
    
    public UserResponse getUserByIdFallback(Long id, Exception ex) {
        log.warn("使用降级策略获取用户信息: {}", id, ex);
        // 返回缓存数据或默认值
        return UserResponse.builder()
                .id(id)
                .username("unknown")
                .build();
    }
}

7. 总结与展望

7.1 核心要点回顾

本文详细介绍了Spring Boot异常处理的完整解决方案:

1. 分层异常设计:构建清晰的异常继承体系,便于精细化处理

2. 全局异常处理:使用@RestControllerAdvice集中处理各类异常

3. 统一响应格式:标准化API返回格式,提升客户端开发体验

4. 结构化日志记录:记录完整的异常上下文,便于问题排查

5. 性能优化策略:通过异常缓存、延迟加载等提升异常处理性能

7.2 异常处理的未来趋势

随着技术的发展,异常处理也在不断演进:

1. 声明式异常处理:通过注解和AOP实现更简洁的异常处理方式

2. AI辅助诊断:利用机器学习自动分析异常模式和根因

3. 响应式异常处理:适应Spring WebFlux等响应式编程模型

4. 异常即代码:将异常处理和错误码作为业务领域模型的一部分

5. 跨语言异常处理:在多语言微服务环境中统一异常处理标准

更多文章一键直达:

Java泛型全方位剖析:从入门到精通的完整指南(上篇)

深入解析MySQL索引高速查询的核心机制与原理

Spring Bean生命周期:从创建到销毁的全过程详解

Redis全栈应用实战:从缓存到分布式系统全场景解析

解密Java ThreadLocal:核心原理、最佳实践与常见陷阱全解析

Java实现Mybatis日志转MySQL可执行SQL的智能转换工具

HTTP协议详解:万维网背后的通信魔法

最近发表
标签列表