作为开篇,这个系列算是对这几年的开发的一些总结。早在实习的时候听过当时导师说,“代码一定要写得优雅,才可以体现水平”。这句话就成了我的码农生涯的信条。在开发中,尽量都可能让代码简洁、优雅。
在不少的项目里都能遇到的是前辈们留下的“遗迹”,仔细查看一番,类似于下面的一些校验代码遍布整个接口层,于是我尝试去优化这些重复但很有必要的代码。一阵鼓捣下来,搜到了validation框架,使用校验注解极大减少了冗余的代码量,还可以使用不同的校验分组还可对不同场景同一个实体对象进行校验,同时开发者也可自行扩展可以支持的校验注解类型。经过两年的实践,觉得还是挺香的!
@RestController
@RequestMapping("/xxx")
public class XXController {
@Autowired
privete xxxServer xxService;
@PostMapping("/aaa")
public String saveUser(@RequestParam("name") String name,
@RequestParam("age") Integer age,
@RequestParam("desc") String description) {
//点个赞,业务逻辑就应该严格控制入参的合法性
if (name.length() < 1 || name.length() > 20) {
throw new IllegalArgumentException("名字长度在1-20");
}
if (age < 0) {
throw new IllegalArgumentException("年龄不能小于0");
}
return "success";
@PostMapping("/aaa")
public String saveUser(@Validated UserDto userDto) {
return "success";
}
}
public class UserDto {
@NotNull(message = "名不能为空")
@Length(min = 1, max = 20, message = "名字长度1-20")
private String name;
@Size(min = 0, message = "年龄不能小于0")
private Integer age;
}复制代码
1.框架简介与实现原理
首先先了解下validation框架的起源,该框架起源于JSR303提案,首先解释下啥是 JSR。
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向
JCP
(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。(类似于我们在使用git的时候提的merge request)
其中,JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode等注解,就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易地开发定制化的 constraint。整个validation框架主要有validator(校验器)+ ValidatorContext(校验器上下文) + ValidatorFactory(校验器工厂) _+ Validation(校验器主体)组成,这些都是接口定义,所有实现的校验实现都是基于这些接口完成。
下面是一个简单的demo,使用的是hibernate-validator。
//1.生成校验器工厂对象
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
//2.生成校验器
Validator validator = factory.getValidator();
//3.进行校验
Set> set = validator.validate(employee);
for (ConstraintViolation constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
复制代码
下面的是主要的方法调用链路,主要关注是校验器与校验过程是怎么解耦的,网上一大堆说这些注解使用的,但是没有真正讲到这个注解跟校验是怎么关联的,其中使用的是抽象工厂模式去生成校验器。
|-> validatorImpl.validate
|-> validatorImpl.validateInContext
|-> validatorImpl.validateConstraintsForCurrentGroup
|-> validateImpl.validateConstraintsForDefaultGroup
|-> validateImpl.validateConstraintsForSingleDefaultGroupElement
|-> validateImpl.validateMetaConstraint
|-> MetaConstraint.validateConstraint
|-> MetaConstraint.doValidateConstraint
|-> ConstraintTree.validateConstraints
|-> SimpleConstraintTree.validateConstraints
|-> ConstraintTree.validateSingleConstraint
|-> NotNullValidator.isValid
//校验注解定义
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class[] groups() default { };
Class[] payload() default { };
}
//非空校验器
public class NullValidator implements ConstraintValidator {
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
return object == null;
}
}
protected final ConstraintValidator
getInitializedConstraintValidator(ValidationContext validationContext, ValueContext valueContext) {
ConstraintValidator validator;
......
validator = validationContext.getConstraintValidatorManager().getInitializedValidator(
validatedValueType,
descriptor,
validationContext.getConstraintValidatorFactory(),
validationContext.getConstraintValidatorInitializationContext()
return validator;
}
public ConstraintValidator getInitializedValidator(
Type validatedValueType,
ConstraintDescriptorImpl descriptor,
ConstraintValidatorFactory constraintValidatorFactory,
HibernateConstraintValidatorInitializationContext initializationContext) {
CacheKey key = new CacheKey( descriptor.getAnnotationDescriptor(),
validatedValueType, constraintValidatorFactory, initializationContext );
ConstraintValidator constraintValidator =
(ConstraintValidator) constraintValidatorCache.get( key );
//通过不同的注解类型生成并缓存校验器
if ( constraintValidator == null ) {
constraintValidator = createAndInitializeValidator( validatedValueType, descriptor, constraintValidatorFactory, initializationContext );
constraintValidator = cacheValidator( key, constraintValidator );
}
return DUMMY_CONSTRAINT_VALIDATOR == constraintValidator ? null : constraintValidator;
}复制代码
2.spring中的应用
2.1 初始化validation
在刚开始的例子中,我们看到了spring也集成了validation框架,它又是怎么实现的呢?首先,spring-context中带了一套validator的框架的扩展实现,通过smartValidator/SpringValidatorAdapter赋予了Validator更多的特性,并在LocalValidatorFactoryBean#afterPropertiesSet方法后进行bean属性初始化后的处理方法,在该方法中同时初始化了校验器生产工厂对象。
public class LocalValidatorFactoryBean extends SpringValidatorAdapter
implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
public void afterPropertiesSet() {
......
//这里加载了hibernate-5.2版本的classloader
if (this.applicationContext != null) {
try {
Method eclMethod = configuration.getClass().getMethod("externalClassLoader", ClassLoader.class);
ReflectionUtils.invokeMethod(eclMethod, configuration, this.applicationContext.getClassLoader());
}
catch (NoSuchMethodException ex) {
}
}
//初始化了校验器生成工厂类对象
ConstraintValidatorFactory targetConstraintValidatorFactory = this.constraintValidatorFactory;
....
}
复制代码
在前文的例子中,校验器在controller处理时起到了作用,那这个动作是在哪里完成的呢?熟悉的spring框架的童鞋就会想到是在处理解析请求的参数时做了这个对象校验,我们看下ModelAttributeMethodProcessor这个方法做了哪些逻辑,下面是spring处理http请求的调用链。
public class ModelAttributeMethodProcessor
implements HandlerMethodArgumentResolver,
HandlerMethodReturnValueHandler {
@Override
public final Object resolveArgument(....) throws Exception {
BindingResult bindingResult = null;
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
//使用校验器对每个参数对象校验
validateIfApplicable(binder, parameter);
bindingResult = binder.getBindingResult();
}
}
public class ModelAttributeMethodProcessor
implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
for (Annotation ann : parameter.getParameterAnnotations()) {
//找出需要校验的属性
Object[] validationHints = determineValidationHints(ann);
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
}
public void validate(Object... validationHints) {
Object target = getTarget();
BindingResult bindingResult = getBindingResult();
//通过不同的类型的校验器进行校验
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}复制代码
这里想介绍下WebDataBinder,其实是作为http请求参数->处理方法的参数对象属性的一个转换器而已,比如http请求里面是个"2020-02-03 10:00:00"字符串,怎么转换成对象属性中LocalDateTime类型呢?spring框架中就有editor帮开发者做了这个转换操作。这里同样是利用了WebDataBind来做校验错误与reponse的转换。
校验器除了在controller层有用,是不是也可用在方法上呢?其实spring框架还注入了方法拦截器,只要在方法上使用了@Validatedj就可以对方法入参进行方法校验了
//方法拦截器
public class MethodValidationInterceptor implements MethodInterceptor {
...
public Object invoke(MethodInvocation invocation) throws Throwable
....
//进行参数校验
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
....
//执行正常逻辑
Object returnValue = invocation.proceed();
....
return returnValue;
}
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class validatedAnnotationType = Validated.class;
public void afterPropertiesSet() {
//定义注解切点,只要是@Validate注解
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
//生成切点通知器
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
}复制代码
4.实战部分
4.1 校验分组的使用
请求参数封住成参数对象,使用groups标签对不同的验证条件进行分组;
//定义请求体封装类
public class MomentQuery extends PageQuery {
@NotEmpty(message = "班级uid不能为空", groups = {QueryGroup.MomentClassBase.class})
@Length(min = 1, max = 32, message = "班级uid长度不能超过32",groups = {QueryGroup.MomentClassBase.class})
private String classUid;
}
//使用分组校验请求参数
@GetMapping(value = "/v1/student/{classUid}/images")
public Object listImage(@Validated(value =
{QueryGroup.MomentStudentBase.class}) MomentQuery query) {
.....
}
//封装校验结果
public static String buildErrorMessage(BindingResult result) {
StringBuilder message = new StringBuilder();
List list = result.getAllErrors();
if (!CollectionUtils.isEmpty(list)) {
Iterator var3 = list.iterator();
while(var3.hasNext()) {
ObjectError elem = (ObjectError)var3.next();
String defaultMessage = elem.getDefaultMessage();
if (StringUtils.isNotEmpty(defaultMessage)) {
message.append(defaultMessage).append(" ");
}
}
}
return message.toString().trim();
}复制代码
4.2 自定义校验器注解
自定义注解其实挺简单的,只要符合JSR303规范即可,框架在进行扫描类结构时,会自动帮开发者去加载校验器Validator。
/**
* 性别约束校验注解
*/
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SexConstraintValidator.class)
public @interface Sex {
String message() default "性别有误";
Class[] groups() default { };
Class[] payload() default { };
}
/**
* 定义逻辑判断
*/
public class SexConstraintValidator implements ConstraintValidator {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && (value.equals("男") || value.equals("女"));
}
}
public class UserDto {
@NotNull(message = "名不能为空")
@Length(min = 1, max = 20, message = "名字长度在1-20")
private String name;
@Size(min = 0, message = "年龄不能小于0")
private Integer age;
@Sex(message = "性别有误")
private String sex;
}复制代码
5.小结
JSR303规范在减少冗余代码,减轻编码负担方面确实做到了很多,而且框架扩展性很强, 校验器不但止spring体系中应用,其实也应用到了RPC框架场景,如我们正在使用的dubbo中。我们团队在校验框架的体系上又完善了多种数据校验场景,并基于培养了大家都有先校验,后业务的代码编写规范,养成了较好的质量意识。