在 Spring Boot 开发中,Controller 作为请求入口,本应是“轻量的交通指挥官”——接收请求、参数校验、路由到 Service、返回响应。但现实中,很多 Controller 逐渐沦为“万能容器”:业务逻辑堆砌、参数验证混乱、异常处理零散、依赖耦合严重,最终变成维护噩梦。
本文基于 SOLID 设计原则,结合大量生产级案例,详细拆解重构 Controller 的 7 大黄金法则,不仅覆盖基础优化,更补充依赖注入、事务管理、测试设计、性能优化的深度技巧,帮你打造“高内聚、低耦合、易测试、可扩展”的优雅 Controller。
一、核心原则:Controller 只做“交通指挥”,不做“业务执行”
Controller 的核心职责应严格限制在 4 件事,超出范围的逻辑必须剥离:
- 接收 HTTP 请求(路径、参数、请求体);
- 校验请求合法性(参数格式、必填项、业务规则初步校验);
- 调用 Service 层执行核心业务;
- 统一封装响应结果(成功/失败、数据/异常信息)。
反例:臃肿的 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 成为瓶颈;
- 协作效率提升:统一的响应格式、参数校验、异常处理,前后端协作更顺畅。
行动步骤(从易到难)
- 定义统一响应模型
ApiResponse和全局异常处理器; - 为所有接口创建 DTO,用 JSR-380 注解完成参数校验;
- 将 Controller 中的业务逻辑迁移到 Service 层,确保 Controller 只做 4 件事;
- 替换字段注入为构造函数注入,拆分万能 Service 为功能单一的接口;
- 引入 MapStruct 自动转换 DTO 与 Entity;
- 编写单元测试和集成测试,确保核心接口可测试;
- 引入缓存和异步处理,优化高频接口性能;
- 定期代码审查,避免反模式回归。
重构不是一蹴而就的,建议从新接口开始遵循这些法则,旧接口逐步迭代优化。最终,你的 Controller 会成为“轻量、优雅、稳定”的请求入口,让开发和维护都变得轻松。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论