SpringBoot项目校验最佳实践:三层防御性编程
·
文章目录
这是一个非常好的问题!@Validated 在 Controller 层、Service 层和 ServiceImpl 层 都使用,是基于 分层校验原则 和 防御性编程 的最佳实践。让我详细解释原因:
1. 为什么要在 Controller 层使用 @Validated?
Controller 层是第一道防线,负责:
- 校验 HTTP 请求参数的格式(格式、长度、非空等)
- 防止恶意输入直接进入系统
- 返回友好的错误信息给前端
@RestController
@RequestMapping("/user")
@Validated // ✅ Controller 层校验
public class UserController {
@PostMapping("/register")
public Result<User> register(@Valid @RequestBody User user, HttpServletRequest request) {
// 这里校验的是 HTTP 请求体的格式
// 例如:用户名不能为空、邮箱格式是否正确等
return userService.register(user, ipAddress);
}
}
2. 为什么还要在 Service 层使用 @Validated?
Service 层是业务逻辑的守门员,负责:
- 校验业务规则的合法性(不仅仅是格式)
- 防止非法业务操作(如负数金额、过去的时间等)
- 确保即使 Controller 层被绕过,业务规则仍然有效
@Service
@Validated // ✅ Service 层校验
public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public User register(@Valid User user, String ipAddress) {
// 这里校验的是业务规则
// 例如:用户名是否已存在、邮箱是否已被注册等
// 即使有人直接调用 Service 方法,也能保证参数合法
return registeredUser;
}
}
3. 为什么 ServiceImpl 层也要使用 @Validated?
ServiceImpl 层是具体实现的保障,负责:
- 实现具体的校验逻辑
- 处理复杂的业务校验规则
- 作为最后的防线,防止任何非法数据进入数据库
@Service
@Validated // ✅ ServiceImpl 层校验
public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public User register(@Valid User user, String ipAddress) {
// 具体的业务校验逻辑
if (checkUsernameExists(user.getUsername())) {
throw new IllegalArgumentException("用户名已存在");
}
// ... 其他业务校验
}
}
4. 三层校验的完整示例
// ==================== Controller 层 ====================
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@PostMapping("/register")
public Result<User> register(@Valid @RequestBody User user) {
// 1. 校验 HTTP 请求格式(@Valid 注解)
// 2. 调用 Service 层
return userService.register(user);
}
}
// ==================== Service 层 ====================
public interface UserService {
@Validated // 接口层也可以定义校验规则
User register(@Valid User user);
}
// ==================== ServiceImpl 层 ====================
@Service
@Validated
public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public User register(@Valid User user) {
// 1. 再次校验业务规则(即使 Controller 层被绕过)
// 2. 执行具体的业务逻辑
// 3. 保存到数据库
return savedUser;
}
}
5. 实际场景中的必要性
场景 1:内部调用
// 定时任务直接调用 Service
@Component
public class UserSyncTask {
@Autowired
private UserService userService;
@Scheduled(cron = "0 0 2 * * ?")
public void syncUsers() {
User user = new User();
user.setUsername("test");
// 如果没有 Service 层校验,这里可能传入非法数据
userService.register(user); // ✅ Service 层会拦截
}
}
场景 2:微服务调用
// 其他服务通过 Feign 调用
@FeignClient(name = "user-service")
public interface UserFeignClient {
@PostMapping("/user/register")
Result<User> register(@RequestBody User user);
}
// 调用方可能没有做参数校验
User user = new User();
user.setUsername(""); // 空用户名
userFeignClient.register(user); // ✅ Service 层会拦截
场景 3:测试代码
@Test
public void testRegister() {
User user = new User();
user.setUsername(null); // 空用户名
userService.register(user); // ✅ Service 层会拦截
}
6. 校验的分工
| 层级 | 校验内容 | 示例 |
|---|---|---|
| Controller 层 | 格式校验 | 用户名不能为空、邮箱格式、长度限制 |
| Service 层 | 业务规则校验 | 用户名是否已存在、邮箱是否已被注册 |
| ServiceImpl 层 | 具体实现校验 | 复杂的业务校验、状态检查 |
7. 避免过度校验
虽然三层都使用 @Validated,但要注意:
- 不要在每层都校验相同的规则(避免重复)
- Controller 层校验格式,Service 层校验业务
- 使用分组校验 来区分不同场景
// 使用分组校验
public interface CreateGroup {}
public interface UpdateGroup {}
public class User {
@NotNull(groups = {CreateGroup.class, UpdateGroup.class})
private Long id;
@NotBlank(groups = CreateGroup.class)
private String username;
}
// Controller 使用 CreateGroup
@PostMapping("/register")
public Result<User> register(@Validated(CreateGroup.class) @RequestBody User user) {
return userService.register(user);
}
// Service 使用 UpdateGroup
@PutMapping("/update")
public Result<User> update(@Validated(UpdateGroup.class) @RequestBody User user) {
return userService.updateUser(user);
}
8. 最佳实践建议
- Controller 层:使用
@Validated+@Valid校验请求参数 - Service 层:使用
@Validated定义业务校验规则 - ServiceImpl 层:实现具体的校验逻辑
- 使用分组校验:区分不同场景的校验规则
- 统一异常处理:使用
@ControllerAdvice统一处理校验异常
总结
三层都使用 @Validated 的原因是:
- Controller 层:保护系统入口,防止非法请求
- Service 层:保护业务逻辑,防止非法操作
- ServiceImpl 层:作为最后防线,确保数据完整性
这是一种 防御性编程 的最佳实践,确保系统在任何一个环节都不会因为非法数据而出错。虽然看起来有些冗余,但在大型系统中,这种分层校验是非常必要的。
更多推荐
所有评论(0)