李锋镝的博客

  • 首页
  • 时间轴
  • 插件
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
  • 留言
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

重构 Controller 终极指南:从臃肿到优雅的 7 大黄金法则 + 实战技巧

2025年10月31日 57点热度 0人点赞 0条评论

在 Spring Boot 开发中,Controller 作为请求入口,本应是“轻量的交通指挥官”——接收请求、参数校验、路由到 Service、返回响应。但现实中,很多 Controller 逐渐沦为“万能容器”:业务逻辑堆砌、参数验证混乱、异常处理零散、依赖耦合严重,最终变成维护噩梦。

本文基于 SOLID 设计原则,结合大量生产级案例,详细拆解重构 Controller 的 7 大黄金法则,不仅覆盖基础优化,更补充依赖注入、事务管理、测试设计、性能优化的深度技巧,帮你打造“高内聚、低耦合、易测试、可扩展”的优雅 Controller。

一、核心原则:Controller 只做“交通指挥”,不做“业务执行”

Controller 的核心职责应严格限制在 4 件事,超出范围的逻辑必须剥离:

  1. 接收 HTTP 请求(路径、参数、请求体);
  2. 校验请求合法性(参数格式、必填项、业务规则初步校验);
  3. 调用 Service 层执行核心业务;
  4. 统一封装响应结果(成功/失败、数据/异常信息)。

反例:臃肿的 Controller(包含 5 类违规逻辑)

@RestController
@RequestMapping("/api/orders")
public class BadOrderController {
    // 直接依赖 Repository(跳过 Service 层)
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private PasswordEncoder passwordEncoder; // 无关依赖
    @Autowired
    private EmailService emailService;

    @PostMapping("/create")
    public ResponseEntity<?> createOrder(@RequestBody Map<String, Object> request) {
        // 1. 参数解析(应使用 DTO)
        Long userId = (Long) request.get("userId");
        List<Map<String, Object>> itemList = (List<Map<String, Object>>) request.get("items");
        if (userId == null || itemList == null || itemList.isEmpty()) {
            return ResponseEntity.badRequest().body("userId 和 items 不能为空");
        }

        // 2. 业务逻辑(应放在 Service)
        // 检查用户是否存在
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("用户不存在"));
        // 检查库存
        for (Map<String, Object> item : itemList) {
            Long productId = (Long) item.get("productId");
            Integer quantity = (Integer) item.get("quantity");
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("商品不存在"));
            if (product.getStock() < quantity) {
                return ResponseEntity.badRequest().body("商品" + productId + "库存不足");
            }
        }

        // 3. 数据组装(应使用 Mapper)
        Order order = new Order();
        order.setUserId(userId);
        order.setOrderNo(UUID.randomUUID().toString());
        order.setStatus(1);
        order.setCreateTime(new Date());
        List<OrderItem> orderItems = itemList.stream()
                .map(item -> {
                    OrderItem orderItem = new OrderItem();
                    orderItem.setProductId((Long) item.get("productId"));
                    orderItem.setQuantity((Integer) item.get("quantity"));
                    orderItem.setOrderId(order.getId()); // 此时 order 未保存,id 为 null(bug)
                    return orderItem;
                })
                .collect(Collectors.toList());
        order.setOrderItems(orderItems);

        // 4. 数据库操作(应放在 Service + Repository)
        orderRepository.save(order);
        // 扣减库存(无事务,可能出现部分扣减成功)
        itemList.forEach(item -> {
            Long productId = (Long) item.get("productId");
            Integer quantity = (Integer) item.get("quantity");
            productRepository.decreaseStock(productId, quantity);
        });

        // 5. 异步操作(无异常处理)
        new Thread(() -> emailService.sendOrderSuccessEmail(user.getEmail(), order.getOrderNo())).start();

        return ResponseEntity.ok(order);
    }
}

正例:重构后的优雅 Controller

@RestController
@RequestMapping("/api/orders")
public class GoodOrderController {
    // 只依赖 Service 层,不直接依赖 Repository 或工具类
    private final OrderService orderService;

    // 构造函数注入(推荐,便于测试和解耦)
    public GoodOrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/create")
    public ApiResponse<OrderResponseDTO> createOrder(@Valid @RequestBody CreateOrderDTO request) {
        // 1. 参数校验(通过 @Valid + DTO 自动完成)
        // 2. 调用 Service 执行业务(核心逻辑剥离)
        OrderResponseDTO order = orderService.createOrder(request);
        // 3. 统一封装响应(全局响应格式)
        return ApiResponse.success(order);
    }
}

关键重构点对比

反例问题 重构方案
直接依赖 Repository 只依赖 Service 层,数据库操作由 Service 调用 Repository
手动解析 Map 请求体 使用 DTO 接收请求,配合 JSR-380 注解校验
业务逻辑(库存检查、扣减)堆砌 所有业务逻辑迁移到 Service 层
手动组装实体 使用 MapStruct 自动转换 DTO ↔ Entity
无事务管理 Service 层添加 @Transactional 保证原子性
手动创建线程异步发送邮件 Service 层使用 @Async + 线程池处理异步任务
零散的异常处理 全局异常处理器统一捕获并返回标准响应

二、法则 1:依赖注入解耦,拒绝“硬耦合”与“过度依赖”

依赖注入是 Spring 的核心,但很多 Controller 仍存在依赖管理混乱的问题,导致测试困难、扩展性差。

1. 推荐构造函数注入,拒绝字段注入

注入方式 代码示例 优点 缺点
字段注入(@Autowired) @Autowired private OrderService orderService; 代码简洁,少写 getter/setter 耦合 Spring 框架、无法手动创建实例、测试困难
构造函数注入 public GoodOrderController(OrderService orderService) { this.orderService = orderService; } 解耦框架、强制依赖初始化、测试易 Mock 代码略多(可通过 Lombok @RequiredArgsConstructor 简化)

实战技巧:结合 Lombok 简化构造函数注入:

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor // 自动生成构造函数(针对 final 字段)
public class OrderController {
    private final OrderService orderService; // final 字段,必须通过构造函数初始化
    private final OrderValidator orderValidator;
}

2. 依赖层级清晰,拒绝跨层依赖

Controller 只能依赖 Service 层,禁止直接依赖以下组件:

  • Repository 层(数据访问层);
  • 工具类(如 PasswordEncoder、RedisTemplate,应封装到 Service 层);
  • 第三方服务(如 EmailService、PaymentService,应通过 Service 层调用)。

3. 接口隔离:按功能拆分 Service,拒绝“万能 Service”

遵循接口隔离原则(ISP),将 Service 按功能拆分,避免一个 Service 承担过多职责:

// 错误:万能 Service(包含查询、创建、更新、删除所有逻辑)
public interface OrderService {
    OrderDTO getById(Long id);
    List<OrderDTO> listAll();
    OrderDTO create(CreateOrderDTO dto);
    OrderDTO update(Long id, UpdateOrderDTO dto);
    void delete(Long id);
    void cancel(Long id);
    void pay(Long id, PayDTO dto);
}

// 正确:按功能拆分接口
public interface OrderQueryService { // 查询相关
    OrderDTO getById(Long id);
    List<OrderDTO> listByUserId(Long userId);
    Page<OrderDTO> page(OrderPageQueryDTO query);
}

public interface OrderCommandService { // 写操作相关(创建、更新、删除)
    OrderDTO create(CreateOrderDTO dto);
    OrderDTO update(Long id, UpdateOrderDTO dto);
    void cancel(Long id);
    void pay(Long id, PayDTO dto);
}

// Controller 按需依赖
@RestController
@RequiredArgsConstructor
public class OrderController {
    private final OrderQueryService orderQueryService;
    private final OrderCommandService orderCommandService;

    @GetMapping("/{id}")
    public ApiResponse<OrderDTO> getOrder(@PathVariable Long id) {
        return ApiResponse.success(orderQueryService.getById(id));
    }

    @PostMapping
    public ApiResponse<OrderDTO> createOrder(@Valid @RequestBody CreateOrderDTO dto) {
        return ApiResponse.success(orderCommandService.create(dto));
    }
}

4. 依赖注入的测试友好性

构造函数注入的核心优势是“测试方便”,可通过 Mockito 轻松 Mock 依赖:

@ExtendWith(MockitoExtension.class)
class OrderControllerTest {
    // Mock 依赖的 Service
    @Mock
    private OrderQueryService orderQueryService;
    @Mock
    private OrderCommandService orderCommandService;

    // 注入到 Controller(手动创建实例,不依赖 Spring 容器)
    @InjectMocks
    private OrderController orderController;

    @Test
    void getOrder_shouldReturnSuccess() {
        // Arrange
        Long orderId = 1L;
        OrderDTO mockOrder = new OrderDTO();
        mockOrder.setId(orderId);
        mockOrder.setOrderNo("TEST123");
        when(orderQueryService.getById(orderId)).thenReturn(mockOrder);

        // Act
        ApiResponse<OrderDTO> response = orderController.getOrder(orderId);

        // Assert
        assertThat(response.isSuccess()).isTrue();
        assertThat(response.getData().getOrderNo()).isEqualTo("TEST123");
        verify(orderQueryService, times(1)).getById(orderId);
    }
}

三、法则 2:参数校验标准化,拒绝“手动校验”与“参数混乱”

参数校验是 Controller 的核心职责之一,但很多代码仍用大量 if-else 手动校验,导致代码冗余、易出错。

1. 用 DTO 接收请求,拒绝 Map/JSONObject

请求参数必须通过 DTO 接收,禁止直接使用 Map 或 JSONObject,原因:

  • 类型安全:避免手动转换类型的错误(如 Long 转 String 失败);
  • 可读性强:通过字段名和注释明确参数含义;
  • 便于校验:配合 JSR-380 注解自动校验。

2. JSR-380 注解实现自动校验

JSR-380 是 Java 规范的参数校验注解,配合 Spring 的 @Valid 或 @Validated 可自动完成校验:

常用校验注解

注解 用途 示例
@NotNull 字段不能为空 @NotNull(message = "用户ID不能为空") private Long userId;
@NotBlank 字符串不能为空且长度>0(忽略空格) @NotBlank(message = "订单号不能为空") private String orderNo;
@Size 集合/字符串长度在指定范围 @Size(min = 1, message = "至少选择1件商品") private List<OrderItemDTO> items;
@Min/@Max 数字类型的最小值/最大值 @Min(value = 1, message = "数量至少为1") private Integer quantity;
@Pattern 字符串匹配正则表达式 @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") private String phone;
@Valid 嵌套校验(如 DTO 中的子 DTO) @Valid private List<OrderItemDTO> items;

实战:CreateOrderDTO 完整示例

@Data
public class CreateOrderDTO {
    @NotNull(message = "用户ID不能为空")
    private Long userId;

    @Valid // 嵌套校验子 DTO
    @Size(min = 1, message = "至少选择1件商品")
    private List<OrderItemDTO> items;

    @NotNull(message = "收货地址ID不能为空")
    private Long addressId;

    @Pattern(regexp = "^(WECHAT|PAYPAL|CARD)$", message = "支付方式只能是 WECHAT/PAYPAL/CARD")
    private String payType;

    // 子 DTO 校验
    @Data
    public static class OrderItemDTO {
        @NotNull(message = "商品ID不能为空")
        private Long productId;

        @Min(value = 1, message = "购买数量至少为1")
        @Max(value = 100, message = "购买数量最多100件")
        private Integer quantity;
    }
}

3. 全局异常处理器统一处理校验失败

校验失败后,Spring 会抛出 MethodArgumentNotValidException,需通过 @ControllerAdvice 全局捕获并返回标准响应:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    // 处理参数校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException e) {
        // 提取所有校验失败信息
        List<String> errors = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.toList());
        log.warn("参数校验失败:{}", errors);
        return ResponseEntity.badRequest()
                .body(ApiResponse.error("参数校验失败", errors));
    }

    // 处理业务异常(自定义异常)
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {
        log.warn("业务异常:{}", e.getMessage());
        return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
    }

    // 处理未捕获的异常(500)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleUncaughtException(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("系统繁忙,请稍后再试"));
    }
}

4. 复杂业务校验:自定义校验注解

简单校验用 JSR-380 注解,复杂业务校验(如“优惠券是否过期”“用户是否有购买权限”)可自定义校验注解:

// 1. 自定义校验注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CouponValidValidator.class) // 指定校验器
public @interface CouponValid {
    String message() default "优惠券无效";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. 实现校验器
public class CouponValidValidator implements ConstraintValidator<CouponValid, String> {
    @Autowired
    private CouponService couponService;

    @Override
    public boolean isValid(String couponCode, ConstraintValidatorContext context) {
        if (couponCode == null) {
            return true; // 非必填字段,null 时通过校验
        }
        // 业务校验:优惠券是否存在、是否过期、是否已使用
        return couponService.isValid(couponCode);
    }
}

// 3. 在 DTO 中使用
@Data
public class CreateOrderDTO {
    // ... 其他字段
    @CouponValid(message = "优惠券无效(过期/已使用/不存在)")
    private String couponCode; // 可选字段,非 null 时触发校验
}

四、法则 3:响应格式标准化,拒绝“零散响应”与“状态码混乱”

不同接口返回格式不一致(有的返回 Map,有的返回实体,有的直接抛异常),会导致前端处理复杂、调试困难。需定义全局统一的响应格式。

1. 统一响应模型设计

@Data
public class ApiResponse<T> {
    // 响应状态(true-成功,false-失败)
    private boolean success;
    // 响应消息(失败时返回错误信息)
    private String message;
    // 响应数据(成功时返回)
    private T data;
    // 时间戳(便于调试)
    private long timestamp;

    // 成功响应(带数据)
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setData(data);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }

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

    // 失败响应(带错误信息)
    public static <T> ApiResponse<T> error(String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage(message);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }

    // 失败响应(带错误信息和详情)
    public static <T> ApiResponse<T> error(String message, List<String> details) {
        ApiResponse<T> response = error(message);
        // 可添加 details 字段存储详细错误信息
        return response;
    }
}

2. 响应状态码规范

遵循 HTTP 状态码语义,避免随意使用: 状态码 含义 适用场景
200 OK 请求成功 所有正常响应(包括查询、创建、更新成功)
400 Bad Request 请求参数错误或业务逻辑错误 参数校验失败、库存不足、优惠券无效等
401 Unauthorized 未授权(未登录或 Token 失效) 未登录访问需要授权的接口
403 Forbidden 权限不足 已登录但无操作权限(如普通用户删除管理员数据)
404 Not Found 资源不存在 查询的订单/商品/用户不存在
500 Internal Server Error 系统内部错误 未捕获的异常、数据库错误等

3. 分页响应标准化

分页查询需返回统一的分页模型,避免前端手动计算总页数:

@Data
public class PageResponseDTO<T> {
    // 数据列表
    private List<T> records;
    // 总条数
    private long total;
    // 总页数
    private long pages;
    // 当前页
    private long current;
    // 每页条数
    private long size;

    // 从 MyBatis-Plus Page 转换
    public static <T> PageResponseDTO<T> fromPage(IPage<T> page) {
        PageResponseDTO<T> response = new PageResponseDTO<>();
        response.setRecords(page.getRecords());
        response.setTotal(page.getTotal());
        response.setPages(page.getPages());
        response.setCurrent(page.getCurrent());
        response.setSize(page.getSize());
        return response;
    }
}

// Controller 中使用
@GetMapping("/page")
public ApiResponse<PageResponseDTO<OrderDTO>> pageOrders(OrderPageQueryDTO query) {
    IPage<OrderDTO> page = orderQueryService.page(query);
    return ApiResponse.success(PageResponseDTO.fromPage(page));
}

五、法则 4:业务逻辑剥离,Service 层只做“业务执行者”

Controller 臃肿的核心原因是业务逻辑堆砌,需将所有核心业务(数据校验、事务处理、第三方调用)迁移到 Service 层,且 Service 层需遵循“单一职责”。

1. Service 层职责划分

层级 核心职责 示例
Controller 请求接收、参数校验、响应封装 接收创建订单请求,调用 Service 创建订单
Service 业务逻辑(库存检查、订单创建、库存扣减)、事务管理、第三方调用 校验库存→创建订单→扣减库存→发送通知
Repository 数据访问(CRUD)、SQL 执行 保存订单、扣减商品库存

2. Service 层事务管理技巧

事务管理必须在 Service 层实现,Controller 层禁止处理事务:

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderCommandServiceImpl implements OrderCommandService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final OrderItemRepository orderItemRepository;
    private final CouponService couponService;
    private final OrderMapper orderMapper;

    @Override
    @Transactional(rollbackFor = Exception.class) // 所有异常都回滚(默认只回滚 RuntimeException)
    public OrderResponseDTO createOrder(CreateOrderDTO request) {
        // 1. 业务校验(比 Controller 更深入的校验)
        UserDTO user = userService.getById(request.getUserId());
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        // 校验优惠券(如果有)
        if (StringUtils.hasText(request.getCouponCode())) {
            boolean couponValid = couponService.useCoupon(request.getUserId(), request.getCouponCode());
            if (!couponValid) {
                throw new BusinessException("优惠券使用失败");
            }
        }

        // 2. 扣减库存(事务内,原子操作)
        for (CreateOrderDTO.OrderItemDTO item : request.getItems()) {
            int affected = productRepository.decreaseStock(item.getProductId(), item.getQuantity());
            if (affected == 0) {
                throw new BusinessException("商品" + item.getProductId() + "库存不足");
            }
        }

        // 3. 创建订单(Entity 组装)
        Order order = orderMapper.toEntity(request);
        order.setOrderNo(generateOrderNo());
        order.setStatus(OrderStatus.PENDING);
        order.setCreateTime(LocalDateTime.now());
        Order savedOrder = orderRepository.save(order);

        // 4. 创建订单项(关联订单 ID)
        List<OrderItem> orderItems = request.getItems().stream()
                .map(item -> {
                    OrderItem orderItem = new OrderItem();
                    orderItem.setOrderId(savedOrder.getId());
                    orderItem.setProductId(item.getProductId());
                    orderItem.setQuantity(item.getQuantity());
                    // 从商品表查询单价(避免前端传参篡改)
                    Product product = productRepository.findById(item.getProductId()).get();
                    orderItem.setPrice(product.getPrice());
                    return orderItem;
                })
                .collect(Collectors.toList());
        orderItemRepository.saveAll(orderItems);

        // 5. 异步发送通知(事务外执行,不阻塞主流程)
        orderNotificationService.sendCreateSuccessNotification(savedOrder.getId());

        // 6. 转换为 DTO 返回
        return orderMapper.toResponseDTO(savedOrder, orderItems);
    }

    // 生成唯一订单号(年月日+随机数)
    private String generateOrderNo() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) 
                + RandomStringUtils.randomNumeric(8);
    }
}

3. 事务管理关键注意事项

  • 指定回滚异常:@Transactional(rollbackFor = Exception.class) 确保所有异常(包括受检异常)都回滚;
  • 避免事务失效:Service 方法必须是 public、不能被 final 修饰、不能内部调用(AOP 无法拦截);
  • 事务传播机制:复杂场景(如调用其他 Service 方法)需指定传播机制(如 propagation = Propagation.REQUIRED,默认值,适合大多数场景);
  • 只读事务:查询方法添加 @Transactional(readOnly = true),提升数据库性能。

六、法则 5:数据转换自动化,拒绝“手动硬编码”

Controller 与 Service 之间、Service 与 Repository 之间的数据流转换(DTO ↔ Entity)是重复劳动的重灾区,且易出错。

1. 推荐 MapStruct,拒绝手动转换

MapStruct 是基于注解的代码生成器,能自动生成 DTO 与 Entity 的转换代码,比手动转换更高效、更不易出错。

步骤 1:引入依赖(Maven)

<!-- MapStruct 核心依赖 -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<!-- 编译期生成代码 -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

步骤 2:定义 Mapper 接口

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface OrderMapper {
    // DTO → Entity(创建订单)
    @Mapping(target = "id", ignore = true) // 主键自增,忽略
    @Mapping(target = "orderNo", ignore = true) // 订单号由 Service 生成,忽略
    @Mapping(target = "status", expression = "java(com.example.order.enums.OrderStatus.PENDING)") // 固定初始状态
    @Mapping(target = "createTime", expression = "java(java.time.LocalDateTime.now())") // 自动填充创建时间
    Order toEntity(CreateOrderDTO dto);

    // Entity + 订单项 → 响应 DTO
    @Mapping(source = "orderItems", target = "items")
    @Mapping(source = "order.totalAmount", target = "totalAmount")
    OrderResponseDTO toResponseDTO(Order order, List<OrderItem> orderItems);

    // 订单项 Entity → DTO
    OrderItemDTO toOrderItemDTO(OrderItem orderItem);
}

步骤 3:使用 Mapper 转换

@Service
@RequiredArgsConstructor
public class OrderCommandServiceImpl implements OrderCommandService {
    private final OrderMapper orderMapper;

    @Override
    public OrderResponseDTO createOrder(CreateOrderDTO request) {
        // DTO → Entity(自动转换)
        Order order = orderMapper.toEntity(request);
        // ... 业务逻辑 ...
        // Entity → DTO(自动转换)
        return orderMapper.toResponseDTO(savedOrder, orderItems);
    }
}

2. 转换技巧:解决字段不匹配与复杂映射

  • 字段名不一致:用 @Mapping(source = "userId", target = "userIdentifier") 指定映射关系;
  • 复杂逻辑映射:用 expression 注入自定义代码(如日期格式化、枚举转换);
  • 集合转换:MapStruct 自动支持 List ↔ List,无需手动循环。

七、法则 6:测试友好设计,拒绝“难以测试”的 Controller

Controller 作为请求入口,需保证可测试性,否则难以验证功能正确性和兼容性。

1. 单元测试:Mock Service 层

使用 @WebMvcTest 专注测试 Controller 层,Mock 依赖的 Service:

@WebMvcTest(OrderController.class)
@ExtendWith(MockitoExtension.class)
class OrderControllerWebTest {
    // 自动注入 MockMvc(模拟 HTTP 请求)
    @Autowired
    private MockMvc mockMvc;

    // Mock 依赖的 Service
    @MockBean
    private OrderCommandService orderCommandService;

    // 测试创建订单接口
    @Test
    void createOrder_shouldReturnSuccess() throws Exception {
        // Arrange
        CreateOrderDTO request = new CreateOrderDTO();
        request.setUserId(1L);
        CreateOrderDTO.OrderItemDTO item = new CreateOrderDTO.OrderItemDTO();
        item.setProductId(1001L);
        item.setQuantity(2);
        request.setItems(Collections.singletonList(item));
        request.setAddressId(10L);
        request.setPayType("WECHAT");

        OrderResponseDTO mockResponse = new OrderResponseDTO();
        mockResponse.setOrderNo("20240520123456");
        mockResponse.setStatus("PENDING");
        when(orderCommandService.createOrder(any(CreateOrderDTO.class))).thenReturn(mockResponse);

        // Act & Assert(模拟 POST 请求)
        mockMvc.perform(post("/api/orders/create")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data.orderNo").value("20240520123456"))
                .andExpect(jsonPath("$.data.status").value("PENDING"));

        // 验证 Service 被调用
        verify(orderCommandService, times(1)).createOrder(any(CreateOrderDTO.class));
    }

    // 测试参数校验失败场景
    @Test
    void createOrder_withInvalidParam_shouldReturnBadRequest() throws Exception {
        // Arrange:userId 为 null,items 为空
        CreateOrderDTO request = new CreateOrderDTO();
        request.setUserId(null);
        request.setItems(Collections.emptyList());

        // Act & Assert
        mockMvc.perform(post("/api/orders/create")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.message").value("参数校验失败"))
                .andExpect(jsonPath("$.details").contains("userId: 用户ID不能为空"))
                .andExpect(jsonPath("$.details").contains("items: 至少选择1件商品"));

        // 验证 Service 未被调用
        verify(orderCommandService, never()).createOrder(any());
    }
}

2. 集成测试:验证端到端流程

使用 @SpringBootTest + TestRestTemplate 测试完整流程(Controller → Service → Repository):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 使用真实数据库
@Transactional // 测试后回滚数据
class OrderControllerIntegrationTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void createOrder_completeFlow_shouldSuccess() {
        // Arrange:初始化测试数据(商品库存充足)
        Product product = new Product();
        product.setId(1001L);
        product.setName("测试商品");
        product.setPrice(new BigDecimal("99.99"));
        product.setStock(100);
        productRepository.save(product);

        // 构造请求
        CreateOrderDTO request = new CreateOrderDTO();
        request.setUserId(1L);
        CreateOrderDTO.OrderItemDTO item = new CreateOrderDTO.OrderItemDTO();
        item.setProductId(1001L);
        item.setQuantity(2);
        request.setItems(Collections.singletonList(item));
        request.setAddressId(10L);
        request.setPayType("WECHAT");

        // Act:发送请求
        ApiResponse<OrderResponseDTO> response = restTemplate.postForObject(
                "/api/orders/create", request, new ParameterizedTypeReference<ApiResponse<OrderResponseDTO>>() {});

        // Assert:响应正确
        assertThat(response.isSuccess()).isTrue();
        assertThat(response.getData().getOrderNo()).isNotNull();

        // Assert:数据库状态正确(库存扣减)
        Product updatedProduct = productRepository.findById(1001L).get();
        assertThat(updatedProduct.getStock()).isEqualTo(98); // 100 - 2 = 98
    }
}

3. 测试覆盖率提升技巧

  • 覆盖核心场景:正常成功、参数校验失败、业务逻辑失败(库存不足)、资源不存在;
  • 避免测试实现细节:只测试输入输出,不测试 Service 内部逻辑(由 Service 自身测试覆盖);
  • 使用测试工具:JaCoCo 统计测试覆盖率,确保核心接口覆盖率≥80%。

八、法则 7:性能优化,让 Controller 更快更稳

优雅的 Controller 不仅要结构清晰,还要性能优异,避免成为系统瓶颈。

1. 缓存热点数据,减少重复查询

对于高频查询接口(如商品详情、订单列表),使用 Spring Cache 缓存结果:

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
    private final ProductQueryService productQueryService;

    // 缓存商品详情(key 为商品 ID,缓存 10 分钟)
    @GetMapping("/{id}")
    @Cacheable(value = "product:detail", key = "#id", unless = "#result == null")
    public ApiResponse<ProductDTO> getProductDetail(@PathVariable Long id) {
        ProductDTO product = productQueryService.getById(id);
        return ApiResponse.success(product);
    }

    // 更新商品后清除缓存(避免缓存脏读)
    @PutMapping("/{id}")
    @CacheEvict(value = "product:detail", key = "#id")
    public ApiResponse<ProductDTO> updateProduct(@PathVariable Long id, @Valid @RequestBody UpdateProductDTO request) {
        ProductDTO product = productCommandService.update(id, request);
        return ApiResponse.success(product);
    }
}

// 缓存配置(Redis 作为缓存介质)
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 默认缓存 10 分钟
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues(); // 不缓存 null 值

        // 针对不同缓存设置不同过期时间
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("product:detail", config.entryTtl(Duration.ofHours(1))); // 商品详情缓存 1 小时
        cacheConfigurations.put("product:list", config.entryTtl(Duration.ofMinutes(30))); // 商品列表缓存 30 分钟

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }
}

2. 异步处理耗时操作,避免阻塞

对于耗时操作(如发送邮件、生成报表、调用第三方接口),使用异步处理避免阻塞主线程:

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
    private final OrderCommandService orderCommandService;
    private final OrderAsyncService orderAsyncService;

    @PostMapping("/create")
    public ApiResponse<OrderResponseDTO> createOrder(@Valid @RequestBody CreateOrderDTO request) {
        OrderResponseDTO order = orderCommandService.createOrder(request);
        // 异步发送通知(不阻塞响应)
        orderAsyncService.sendCreateSuccessNotification(order.getOrderNo(), request.getUserId());
        return ApiResponse.success(order);
    }
}

// 异步 Service
@Service
@Async("orderAsyncPool") // 指定线程池
public class OrderAsyncServiceImpl implements OrderAsyncService {
    @Autowired
    private EmailService emailService;
    @Autowired
    private UserQueryService userQueryService;

    @Override
    public void sendCreateSuccessNotification(String orderNo, Long userId) {
        try {
            UserDTO user = userQueryService.getById(userId);
            emailService.sendOrderSuccessEmail(user.getEmail(), orderNo);
        } catch (Exception e) {
            log.error("发送订单成功通知失败,orderNo:{}", orderNo, e);
            // 可选:加入重试队列
        }
    }
}

// 异步线程池配置
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "orderAsyncPool")
    public Executor orderAsyncPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 核心线程数
        executor.setMaxPoolSize(10); // 最大线程数
        executor.setQueueCapacity(50); // 队列容量
        executor.setKeepAliveSeconds(60); // 空闲线程存活时间
        executor.setThreadNamePrefix("order-async-"); // 线程名前缀
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略(调用者执行)
        executor.initialize();
        return executor;
    }
}

3. 其他性能优化技巧

  • 请求参数优化:分页查询必传 page/size,避免返回大量数据;
  • 响应压缩:开启 GZIP 压缩(Spring Boot 配置 server.compression.enabled=true),减少传输体积;
  • 接口限流:使用 Spring Cloud Gateway 或 Sentinel 对高频接口限流,避免雪崩;
  • 懒加载:复杂响应数据(如订单详情中的商品详情)按需加载,避免冗余数据传输。

九、常见反模式与重构方案

反模式 典型表现 重构方案
上帝对象 Controller 一个 Controller 包含多个业务模块逻辑(用户+订单+商品) 按业务模块拆分 Controller(UserController、OrderController)
直接操作数据库 Controller 依赖 Repository,手动执行 SQL 所有数据库操作通过 Service → Repository 完成
硬编码常量 响应消息、状态码、路径硬编码 提取常量类(如 ResponseMessage、ApiPath)、枚举(OrderStatus)
异常直接抛出不处理 业务异常直接抛 RuntimeException,前端接收 500 自定义 BusinessException,全局异常处理器统一处理
接口版本混乱 不同版本接口混在一个 Controller,路径无区分 接口版本控制(如 /api/v1/orders、/api/v2/orders)

十、总结:重构 Controller 的核心收益与行动步骤

核心收益

  • 可维护性提升:职责清晰,新增需求只需修改 Service 层,无需动 Controller;
  • 可测试性提升:依赖注入+Mock 测试,快速验证功能正确性;
  • 扩展性提升:接口隔离+依赖解耦,轻松支持多实现、多版本;
  • 性能更稳定:缓存+异步+限流,避免 Controller 成为瓶颈;
  • 协作效率提升:统一的响应格式、参数校验、异常处理,前后端协作更顺畅。

行动步骤(从易到难)

  1. 定义统一响应模型 ApiResponse 和全局异常处理器;
  2. 为所有接口创建 DTO,用 JSR-380 注解完成参数校验;
  3. 将 Controller 中的业务逻辑迁移到 Service 层,确保 Controller 只做 4 件事;
  4. 替换字段注入为构造函数注入,拆分万能 Service 为功能单一的接口;
  5. 引入 MapStruct 自动转换 DTO 与 Entity;
  6. 编写单元测试和集成测试,确保核心接口可测试;
  7. 引入缓存和异步处理,优化高频接口性能;
  8. 定期代码审查,避免反模式回归。

重构不是一蹴而就的,建议从新接口开始遵循这些法则,旧接口逐步迭代优化。最终,你的 Controller 会成为“轻量、优雅、稳定”的请求入口,让开发和维护都变得轻松。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/hou-duan/4551

相关文章

  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • 解锁 Spring Boot 10 个高频 "神仙功能"
  • Spring WebFlux深度解析:异步非阻塞架构与实战落地指南
  • Java进阶实战:10个高效技巧+环境管理指南,让代码简洁又优雅
  • SpringBoot日志链路追踪深度实战:基于 TraceId 打通全链路日志
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: Controller JAVA SpringBoot 重构
最后更新:2025年10月31日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

位卑未敢忘忧国,事定犹须待阖棺。

那年今日(12月17日)

  • 1981年:德国足球运动员蒂姆·维泽出生
  • 1971年:印度和东巴基斯坦达成停火协议
  • 1909年:比利时国王利奥波德二世逝世
  • 1905年:狙击之王西蒙·海耶出生
  • 1902年:京师大学堂正式开学
  • 更多历史事件
最新 热点 随机
最新 热点 随机
AI原生数据库新标杆:seekdb深度解析,轻量架构与混合搜索的双重革命 做了一个WordPress文章热力图插件 Spring WebFlux底层原理深度剖析-从响应式流到事件循环的全链路拆解 Spring WebFlux深度解析:异步非阻塞架构与实战落地指南 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环 WordPress网站换了个字体,差点儿把样式换崩了
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?使用WireGuard在Ubuntu 24.04系统搭建VPNWordPress实现用户评论等级排行榜插件Gemini 3 Pro 深度测评:多模态AI编程的跨代际突破,从一句话到完整应用的全链路革命WordPress网站换了个字体,差点儿把样式换崩了
使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 项目中不用 redis 分布式锁,怎么防止用户重复提交? SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports JAVA线程池简析(JDK1.6) IDEA版本2020.*全局MAVEN配置 Gemini 3 深度解析:从像素级复刻到 AGI 雏形,多模态 AI 如何重构开发与创作?
标签聚合
JVM WordPress SQL 日常 K8s 架构 SpringBoot AI编程 MySQL ElasticSearch 多线程 分布式 数据库 AI JAVA docker 设计模式 Spring IDEA Redis
友情链接
  • Blogs·CN
  • Honesty
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3