跳到主要内容

从来都是在controller层进行参数校验,便对吗

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术

传统的参数校验

相信现在的大部分web项目进行参数校验都是通过如下方式

  1. 添加依赖
		<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 在 controller进行参数校验
  • ValidationController
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class ValidationController {

private final XiaoZouService xiaoZouService;

@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody XiaoZouDTO xiaoZouDTO) {
xiaoZouService.createUser(xiaoZouDTO);
return ResponseEntity.ok("用户创建成功");
}
}
  • XiaoZouDTO
@Data
public class XiaoZouDTO {

@NotNull(message = "用户ID不能为空")
private Long id;

@NotBlank(message = "用户名不能为空")
@Length(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

实际的业务逻辑处理被封装在xiaoZouService

异常处理器

我们再随便加一个全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationExceptions(MethodArgumentNotValidException ex) {
StringBuilder errors = new StringBuilder();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.append(fieldName).append(": ").append(errorMessage).append("; ");
});
return ResponseEntity.badRequest().body(errors.toString());
}
}

测试

然后我们进行接口请求

POST http://localhost:8091/api/xiao-zou
Accept: application/json
Content-Type: application/json

{
"name": "xiao-zou",
"age": 18
}

参数我们随便乱传,就会出现如下错误信息提示

email: 邮箱不能为空; id: 用户ID不能为空; username: 用户名不能为空; 

结果看似满足我们的预期。

但是目前我们会注意到一个问题

  1. 我们的参数实际使用是在service,但是我们的参数校验是在controller
  2. 如果我们存在自定义aop切面进行参数修改,aop的切面执行顺序在参数校验之后,那么我们的参数校验就失效了
  3. 我们的service方法并不单单仅在controller中调用,我们的service方法可能会在其他地方调用,比如jobmqRPC等等,这种情况下我们的参数校验就失效了

我们来实际举例说明

AOP切面修改参数导致的参数校验失败

假设我们的的邮箱为了数据安全之类的业务背景,统一通过请求头获取

那么我们可能定义如下一个切面

@Aspect
@Slf4j
@RequiredArgsConstructor
public class XiaoZouHttpAspect {

@Pointcut("@annotation(com.spring.boot.base.annotation.XiaoZouResponse)")
public void controllerMethodAspect() {

}

@GetMapping
@Before("controllerMethodAspect()")
public void doBefore(JoinPoint joinPoint) {

Object[] args = joinPoint.getArgs();
if (Objects.isNull(args) || args.length == 0) {
return;
}
for (Object argObj : args) {
if (argObj instanceof XiaoZouDTO) {
XiaoZouDTO xiaoZouDTO = (XiaoZouDTO) argObj;
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = requestAttributes.getRequest();
xiaoZouDTO.setEmail(httpServletRequest.getHeader("x-xiaozou-email"));
}
}

}

}

此时如果请求头里面没有x-xiaozou-email,但是我们的body里面传了email

这时候的参数校验也会失败,失败步骤如下

  1. XiaoZouDTO接收到body的email参数进行参数校验通过了参数校验
  2. XiaoZouHttpAspect切面修改了email参数,但是这个修改是在参数校验之后的,email为空进入到service层,所以参数校验失败

dubbo调用参数校验失效

dubbo例的测试用就很简单了。我们的service可能既给controller调用,也可能给dubbo调用

@DubboService
public class XiaoZouServiceImpl implements XiaoZouService {

@Override
public String createUser(XiaoZouDTO xiaoZouDTO) {
return name;
}

}

dubbo过来的请求不会经过我们在controller的参数校验,所以参数校验失效

这里的业务逻辑处理就会因为必要的参数为空出现不符合我们预期的业务逻辑

MQ、xxl-job参数校验失败

MQxxl-job等等的调用也是一样的,我们的参数校验只会在controller中生效,其他地方调用就会失效

最佳实践(解决方式)

很明显最佳的参数校验并不是在controller中进行,而是在service中进行

因为我们的参数实际是给service使用的,我们的很多请求也会绕过controller导致service参数校验失效

所以最佳方式应该是在接口层service进行参数校验

如果我们想要对XiaoZouService进行参数校验,我们应该使用如下方式

@Validated
public interface XiaoZouService {

boolean createUser(@Valid XiaoZouDTO xiaoZouDTO);
}


  1. 我们在service接口上加上@Validated注解
  2. service方法上加上@Valid注解

这样我们的参数校验就会在service层生效了。

不管请求是从controllerdubbomqjob等等过来的,我们的参数校验都会生效。

就不会出现因为参数校验失效导致的业务逻辑出现问题了

总结

目前行业内大部分的教程和规范都是在controller中进行参数校验

导致大家都在controller中进行参数校验。实际这个做法是非常不合理和容易出问题的

如果我们在指定项目规范的时候,我们应该规定参数校验应该在service层进行,而不是在controller层进行