在SpringBoot中参数校验应该写在Controller层还是Service层?
直接上结论:以Controller层为主,Service层为辅。Controller校验请求合规性,Service校验业务逻辑性。
一、为什么Controller层必须做参数校验?Controller是API入口,第一道防线必须在这里构建。不校验的后果是非法参数直接污染业务层。
代码示例:Controller层基础校验 @RestController@RequestMapping("/user")@Validated // 启用校验public class UserController { @PostMapping public Result createUser(@RequestBody @Valid UserDTO userDTO) { // 参数通过校验才会执行到这里 return userService.createUser(userDTO); } @GetMapping("/{id}") public Result getUser(@PathVariable @Min(1) Long id) { // 路径参数也要校验 return userService.getUser(id); }}DTO中的校验注解
public class UserDTO { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20") private String username; @NotNull(message = "年龄必填") @Min(value = 0, message = "年龄不能小于0") @Max(value = 150, message = "年龄不能大于150") private Integer age; @Email(message = "邮箱格式错误") private String email; @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式错误") private String phone;}二、Service层为什么也需要校验?
Service作为业务核心,需要防御性编程。多个入口调用同一Service时,不能依赖Controller已做校验。
场景1:内部方法调用 @Servicepublic class OrderService { public void createOrder(OrderDTO orderDTO) { // 即使Controller校验过,内部方法也要保障 validateOrder(orderDTO); // 业务逻辑... } // 内部方法可能被多个入口调用 public void updateOrder(Long orderId, OrderDTO orderDTO) { // 复用校验逻辑 validateOrder(orderDTO); // 更新逻辑... } private void validateOrder(OrderDTO orderDTO) { if (orderDTO.getAmount() == null || orderDTO.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new BusinessException("订单金额必须大于0"); } // 更复杂的业务规则校验... }}场景2:使用Spring的@Validated进行方法校验
@Service@Validated // Service层也可用public class UserService { public User createUser(@Valid UserDTO userDTO) { // 方法参数会被校验 return userRepository.save(convertToEntity(userDTO)); } // 分组校验示例:更新时的特殊规则 public void updateUser(@Validated(User.UpdateGroup.class) UserDTO userDTO) { // 更新逻辑... }}三、分层校验的最佳实践1.职责清晰划分
Controller层:校验请求数据的格式、类型、范围 ↓Service层:校验业务规则、数据关联性、状态流转 ↓Repository层:数据库约束(唯一索引、外键等)2.实际案例:用户注册流程
// Controller层:格式校验@PostMapping("/register")public Result register(@RequestBody @Valid RegisterDTO dto) { return userService.register(dto);}// Service层:业务校验@Servicepublic class UserService { public Result register(RegisterDTO dto) { // 1. 用户名是否已存在(需要查库) if (userRepository.existsByUsername(dto.getUsername())) { throw new BusinessException("用户名已存在"); } // 2. 邀请码是否有效 if (!inviteCodeService.isValid(dto.getInviteCode())) { throw new BusinessException("无效邀请码"); } // 3. 密码强度(业务规则) if (!passwordValidator.isStrong(dto.getPassword())) { throw new BusinessException("密码强度不足"); } // 创建用户... }}3.使用Assert简化Service校验
@Servicepublic class PaymentService { public void processPayment(PaymentRequest request) { // 使用Spring的Assert工具类 Assert.notNull(request, "支付请求不能为空"); Assert.isTrue(request.getAmount() > 0, "支付金额必须大于0"); // 或使用自定义断言 validatePaymentRequest(request); // 支付逻辑... } private void validatePaymentRequest(PaymentRequest request) { if (request.getUserId() == null) { throw new IllegalArgumentException("用户ID不能为空"); } // 复杂的业务规则校验... }}四、特殊场景处理场景:批量操作校验
@PostMapping("/batch")public Result batchCreate(@RequestBody List<@Valid UserDTO> users) { // 每个元素都会被校验 return userService.batchCreate(users);}@Servicepublic class UserService { public Result batchCreate(List五、总结:黄金法则
校验类型
Controller层
Service层
格式校验
必须做
可选做(防御性)
业务规则
不做
必须做
数据存在性
不做
必须做
权限校验
建议做
必须做
核心原则:- Controller层校验失败 = 客户端请求有问题
- Service层校验失败 = 业务规则不满足
- 永远不要信任外部输入,即使来自"自己"的Controller
- 校验要尽早失败,避免无效操作深入系统
Controller管"输入是否合规",Service管"业务是否允许"。两者互补,缺一不可。
这样的分层校验策略,既能保证API的友好性(快速失败返回明确错误),又能确保业务逻辑的健壮性(防御性编程)。记住:好的校验策略是系统稳定性的第一道保险。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。
