参数校验的几个层级:
- 前端校验
前端校验一般是为了更快速的响应用户输入,通知用户及时纠正错误输入。 - Controller层校验
Controller层直接接受前端传递过来的参数。在这一层建议做参数的“基本校验”。这里的基本校验是指:字段非空判断、数字的最大值和最小值等,这些校验一般是JSR-303注解可以支持的。这里并不进行业务逻辑的校验,理由是Controller层并不做业务逻辑处理,因此不做业务逻辑校验。 - Service层校验
Service层实现业务逻辑,在这一层需要进行业务逻辑的参数校验。此外,当Service层的接口通过网关暴露时,则需要进行基本校验和业务逻辑参数校验。 - DAO层校验
DAO层只能有本地的Service层方法来调用。一般在Service层做参数校验,这一层就不用做校验了。
前面讲过了在Controller层和Service层对于JavaBean和基本参数类型的校验方式。当参数校验不通过时,则默认会抛异常,这是一种处理办法。但是,另一种处理方式是,业务RD会捕获异常,然后返回一个CommonResponse对象。
1 自定义Controller层校验结果
Spring对于Controller层的参数校验支持很完善,可以支持自定义返回结果。
使用步骤如下。
-
引入jar包
引入spring-boot-starter-web,会间接引入validation-api和hibernate-validator等jar包。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.2.RELEASE</version> </dependency> - 定义JavaBean和CommonResponse
JavaBean的定义如下。使用了@Data这个lombok注解来减少开发量。@Data public class Customer { private int customerId; @Min(1) private int age; @Min(1) @Max(2) private int gender; @NotEmpty private String country; @NotEmpty private List<@NotBlank(message = "productNames can not be null") String> productNames; }一般后端返回给前端一个CommonResponse,定义如下。code为状态码,msg为提示信息,result为真正的VO数据。
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResponse <T> { private int code; private String msg; private T result; } - 定义Controller类
@RestController @Validated public class CustomerController { @RequestMapping("/customer/saveCustomer") public void saveCustomer(@Valid Customer customer) { System.out.println(customer); } @RequestMapping("/customer/getCustomer") public Customer getCustomer( @RequestParam("customerId") @Min(1) Integer customerId) { System.out.println(customerId); return new Customer(); } }前面的这3步,与上一篇博客的内容是相同的。
- 自定义全局异常处理类
@ControllerAdvice注解放在类上面,定义了一个Controller的切面类。@ExceptionHandler注解放在方法上,定义了一个异常处理器。它可以带一个属性,用来表明该异常处理器只能对哪些类型的异常进行处理。若为空,则对所有的异常类型进行处理。@ResponseBody表明了返回结果是一个响应实体。@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) @ResponseBody public CommonResponse handleException(Exception ex) { System.out.println(ex); return new CommonResponse(400, "参数错误", null); } @ExceptionHandler(BindException.class) @ResponseBody public CommonResponse handleParamException(Exception ex) { System.out.println(ex); return new CommonResponse(400, "参数错误", null); } }这里,我们统一将ConstraintViolationException和BindException都视为参数错误,返回“参数错误”。前端在很多情况下是不关心具体的错误原因的,这里我们只是记录下来,返回给前端的提示文案就是“参数错误”。
- 测试
- 测试BindException异常的处理器
POST localhost:8080/customer/saveCustomer结果如下
{ "code": 400, "msg": "param error", "result": null } - 测试ConstraintViolationException异常的处理器
GET localhost:8080/customer/getCustomer?customerId=0结果如下
{ "code": 400, "msg": "param error", "result": null }
- 测试BindException异常的处理器
2 自定义Service层校验结果
首先看看@Validated的原理是什么。Service层的使用方式如下所示。
@Service
@Validated
public class CustomerService {
public void saveCustomer(@Valid Customer customer) {
}
public Customer loadCustomer(@Min(1) Integer id, @Valid Customer customer) {
return new Customer();
}
}
当Spring的MethodValidationPostProcessor(一个BeanPostProcessor)发现类上面有@Validated注解时,就会通过下面的方法,创建一个切面(adviser)。
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
在程序运行时,就会运行到MethodValidationInterceptor这个拦截器类的invoke()方法。

可以看出,若参数校验不通过,默认是抛异常的,抛出的异常为ConstraintViolationException。那怎么自定义参数校验的返回结果呢?有两种思路:
- 看看Spring是否支持类似spring web中的ExceptionHandler这样的异常处理器
本人在网上搜索,除了spring web之外,没有提到有这种全局处理器的。 - 模仿Spring的MethodValidationInterceptor实现思路,自己实现一个AOP拦截器。当参数校验不通过时,返回一个Response对象。
本人模仿着Spring处理的逻辑,写了一个AOP,将抛出异常改为返回一个对象。代码见附录A。
3 总结
如果调用方做了异常兼容处理,则可以直接使用默认的处理方式(抛异常)。毕竟,参数校验只是一个防御性质的编程习惯,正常逻辑下的参数都是符合要求的。
如果想要给调用方更好的体验方式,则可以“捕获异常”,将异常包装为一个对象返回给调用方。
附录A
这里只是一个示例,仅供参考。
-
注解定义
package com.chris.validation; // 省略其他说明 import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyValidated { Class<?>[] value() default {}; } - 方法校验拦截器定义
@Component public class MyMethodValidationInterceptor implements MethodInterceptor { private final Validator validator; /** * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. */ public MyMethodValidationInterceptor() { this(Validation.buildDefaultValidatorFactory()); } /** * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory. * @param validatorFactory the JSR-303 ValidatorFactory to use */ public MyMethodValidationInterceptor(ValidatorFactory validatorFactory) { this(validatorFactory.getValidator()); } /** * Create a new MethodValidationInterceptor using the given JSR-303 Validator. * @param validator the JSR-303 Validator to use */ public MyMethodValidationInterceptor(Validator validator) { this.validator = validator; } @Override @SuppressWarnings("unchecked") public Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class<?>[] groups = determineValidationGroups(invocation); // Standard Bean Validation 1.1 API ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; try { result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 // Let's try to find the bridged method on the implementation class... methodToValidate = BridgeMethodResolver.findBridgedMethod( ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass())); result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } if (!result.isEmpty()) { // 这里是处理的重点。原来的方式是throw new ConstraintViolationException(result);这里根据returnType,创建一个对象,然后返回空对象。 Class<?> returnType = invocation.getMethod().getReturnType(); Object retObj = Class.forName(returnType.getName()).newInstance(); CommonResponse response = (CommonResponse) retObj; // 这里直接进行了强转,实际上需要先判断类型。 response.setCode(400); response.setMsg("参数错误"); response.setResult(null); return response; // throw new ConstraintViolationException(result); } Object returnValue = invocation.proceed(); // 这里我们就不校验返回结果了。 // result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); // if (!result.isEmpty()) { // throw new ConstraintViolationException(result); // } return returnValue; } private boolean isFactoryBeanMetadataMethod(Method method) { Class<?> clazz = method.getDeclaringClass(); // Call from interface-based proxy handle, allowing for an efficient check? if (clazz.isInterface()) { return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) && !method.getName().equals("getObject")); } // Call from CGLIB proxy handle, potentially implementing a FactoryBean method? Class<?> factoryBeanType = null; if (SmartFactoryBean.class.isAssignableFrom(clazz)) { factoryBeanType = SmartFactoryBean.class; } else if (FactoryBean.class.isAssignableFrom(clazz)) { factoryBeanType = FactoryBean.class; } return (factoryBeanType != null && !method.getName().equals("getObject") && ClassUtils.hasMethod(factoryBeanType, method.getName(), method.getParameterTypes())); } /** * Determine the validation groups to validate against for the given method invocation. * <p>Default are the validation groups as specified in the {@link Validated} annotation * on the containing target class of the method. * @param invocation the current MethodInvocation * @return the applicable validation groups as a Class array */ protected Class<?>[] determineValidationGroups(MethodInvocation invocation) { Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); if (validatedAnn == null) { validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class); } return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]); } } - BeanPostProcessor定义
@SuppressWarnings("serial") @Component public class MyMethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { private Class<? extends Annotation> validatedAnnotationType = MyValidated.class; @Nullable private Validator validator; /** * Set the 'validated' annotation type. * The default validated annotation type is the {@link Validated} annotation. * <p>This setter property exists so that developers can provide their own * (non-Spring-specific) annotation type to indicate that a class is supposed * to be validated in the sense of applying method validation. * @param validatedAnnotationType the desired annotation type */ public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) { Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null"); this.validatedAnnotationType = validatedAnnotationType; } /** * Set the JSR-303 Validator to delegate to for validating methods. * <p>Default is the default ValidatorFactory's default Validator. */ public void setValidator(Validator validator) { // Unwrap to the native Validator with forExecutables support if (validator instanceof LocalValidatorFactoryBean) { this.validator = ((LocalValidatorFactoryBean) validator).getValidator(); } else if (validator instanceof SpringValidatorAdapter) { this.validator = validator.unwrap(Validator.class); } else { this.validator = validator; } } /** * Set the JSR-303 ValidatorFactory to delegate to for validating methods, * using its default Validator. * <p>Default is the default ValidatorFactory's default Validator. * @see javax.validation.ValidatorFactory#getValidator() */ public void setValidatorFactory(ValidatorFactory validatorFactory) { this.validator = validatorFactory.getValidator(); } @Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } /** * Create AOP advice for method validation purposes, to be applied * with a pointcut for the specified 'validated' annotation. * @param validator the JSR-303 Validator to delegate to * @return the interceptor to use (typically, but not necessarily, * a {@link MethodValidationInterceptor} or subclass thereof) * @since 4.2 */ // 这里使用上面创建的方法校验拦截器类 protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MyMethodValidationInterceptor(validator) : new MyMethodValidationInterceptor()); } } - 使用@MyValidated注解
@Service @MyValidated public class CustomerService { public CommonResponse<Customer> loadCustomer(@Min(1) Integer id) { return new CommonResponse(200, "成功", new Customer()); } }