开发过程中,后台的参数校验是必不可少的,所以经常会看到类似下面这样的代码
这样写并没有什么错,还挺工整的,只是看起来不是很优雅而已。
那么有什么办法可以省去这么繁琐的工作呢?
当然,利用自定义注解和Spring AOP可以做到,参考我的另一篇博客:利用自定义注解和Aspect实现方法参数的非空校验。
但是,自己弄有点重复发明轮子的意思,因为spring已经提供了一套完整的validation,基本上就已经够用了。
一、Spring MVC使用Validator做控制层参数校验
1、在参数类的字段加上校验注解
这里的注解有两种:一种是javax.validation.constraints包里面的,不知道是Spring提供的还是JDK提供的,没去研究;另一种在org.hibernate.validator.constraints包里面,Hibernate提供,需要引入hibernate-validator的jar包,不过已经标识过期了,提示我们用第一种替代。
还有其它各种注解,限于篇幅此处不做介绍,大家可以自己去搜集。
/**
* 用户类
* @author z_hh
* @time 2019年1月18日
*/
@Getter
@Setter
@ToString
public class User {
/** id */
@NotNull(message="id不能为空")
private Long id;
/** 姓名 */
@NotBlank(message="姓名不能为空")
private String name;
/** 年龄 */
@Max(message="年龄不能超过120岁", value = 120)
@Min(message="年龄不能小于0岁", value = 0)
private Integer age;
/** 创建时间 */
@Future
private Date createTime;
}
2、在Controller里需要检验的方法参数加上注解Valid,并定义BindingResult类型参数,然后方法体前面加上处理逻辑
在执行时,Spring会将校验结果保存到bindingResult变量里,我们在代码里面判断处理就可以了。这里为了测试是将所有的错误信息返回。
/**
* 添加用户
* @param user
* @param bindingResult 校验结果收集器
* @return
*/
@PostMapping
public Object add(@Valid @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<ObjectError> objectErrors = bindingResult.getAllErrors();
return objectErrors.stream()
.map(ObjectError::getDefaultMessage)
.reduce((msg1, msg2) -> msg1 + "/" + msg2)
.get();
}
return "添加用户成功!";
}
3、测试一下
可以看到,将不通过的校验的信息打印出来了
这样,就算是解决了参数校验的问题,一切似乎很完美... ...
但是,如果我们每个需要校验的方法代码里面都这样写,是否觉得不太妥?
我觉得有两个问题,第一是代码重复了,第二是跟业务逻辑耦合了。
那有什么方法解决呢?
或许可以将这段代码封装到一个函数,放到一个公共类(或者父类)里面。但是,还是要在每个方法体里面写代码,只是少了一些而已,不算好的方法。
直接敲黑板划重点吧
二、省略方法体内处理校验结果代码的几种方式
第一种,也是我觉得最简单的,就是使用Spring的Aspect。直接上代码
/**
* Validato的切面
* @author z_hh
* @date 2019年1月18日
*/
@Component
@Aspect
public class ValidatorAspect {
/**
* 这里可以具体到包含BindingResult参数的Controller API方法???
*/
@Pointcut("execution(public * cn.zhh.controller.*.*(..))")
private void annotationPointCut() {
}
/**
* 如果@Pointcut匹配到的全部都是需要校验的方法,那么可以省略很多逻辑
*/
@Around("annotationPointCut()")
public Object process(ProceedingJoinPoint pjp) throws Throwable {
// 1、获取目标方法
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
// 2、获取方法参数
Parameter[] parameters = targetMethod.getParameters();
// 3、遍历参数。如果有BindingResult参数,就判断是否校验不通过
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
if (Objects.equals(parameter.getType(), BindingResult.class)) {
BindingResult bindingResult = (BindingResult) pjp.getArgs()[i];
if (bindingResult.hasErrors()) {
List<ObjectError> objectErrors = bindingResult.getAllErrors();
return objectErrors.stream()
.map(ObjectError::getDefaultMessage)
.reduce((msg1, msg2) -> msg1 + "/" + msg2)
.get();
}
}
}
// finish、执行目标方法
return pjp.proceed();
}
}
需要引入AOP的相关依赖。如SpringBoot需要引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二种,在控制类实例化之后注入Spring容器之前修改bean。也就是创建一个实现BeanPostProcessor接口的bean,重写postProcessBeforeInitialization方法。而这里的修改bean,又可以分为两种方式
(1)创建代理对象替换原有对象
动态代理的相关博客:https://blog.csdn.net/qq_31142553/article/details/81489678
代理对象执行的时候判断方法参数是否有BindingResult类型的参数,有就处理。跟第一种类似,此处不再赘述。值得注意的是,因为控制类不一定实现接口,所以最好使用Cglib创建代理对象。
(2)使用Javassist修改字节码,创建新的类及其对象替换原有对象。这里重点讲解一下
Javassist的相关博客:https://blog.csdn.net/qq_31142553/article/details/85395997
首先需要引入jar包依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.24.0-GA</version>
</dependency>
接着需要定义一个类级别注解,表明此类的bean需要参数校验(非必需,也可以根据包名、父类什么的)。这里定义的注解混合了@RestController,让使用起来方便一点,不用写两个。
/**
* 表明该类是控制器,并且需要方法参数校验
* @author z_hh
* @time 2019年1月18日
*/
@Retention(RUNTIME)
@Target(TYPE)
@Documented
@RestController
public @interface RestValidatorController {
@AliasFor(annotation = RestController.class)
String value() default "";
}
最后,就是写bean注入容器前修改字节码创建新对象进行替换的功能了。(这里是重难点)
/**
* 对存在RestValidatorController注解的bean修改字节码
* @author z_hh
* @time 2019年1月18日
*/
@Component
public class AddValidatorCodeProcessor implements BeanPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* 处理参数校验结果的代码,非java.lang包的类需要写全类名。部分代码如foreach、lambda等会编译不通过...
*/
private static final String CODE = "if (bindingResult.hasErrors()) {\r\n" +
" java.util.List objectErrors = bindingResult.getAllErrors();\r\n" +
" StringBuffer sb = new StringBuffer();\r\n" +
" for (int i = 0; i < objectErrors.size(); i++) {\r\n" +
" String msg = ((org.springframework.validation.ObjectError) objectErrors.get(i)).getDefaultMessage();\r\n" +
" sb.append(\"/\" + msg);\r\n" +
" }\r\n" +
" return sb.substring(1);\r\n" +
" }";
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
// 只处理带RestValidatorController注解的类
if (clazz.isAnnotationPresent(RestValidatorController.class)) {
ClassPool pool = ClassPool.getDefault();
String oldName = clazz.getName();
try {
CtClass ctClass = pool.getCtClass(clazz.getName());
// 类名需要改
ctClass.setName(oldName + "$javassist");
CtMethod[] ctMethods = ctClass.getMethods();
// 对存在BindingResult类型参数的方法进行处理:在方法体前面插入代码
for (CtMethod ctMethod : ctMethods) {
CtClass[] parameterTypes = ctMethod.getParameterTypes();
boolean anyMatch = Arrays.stream(parameterTypes)
.anyMatch(parameterType -> Objects.equals(parameterType.getSimpleName(), "BindingResult"));
if (anyMatch) {
ctMethod.insertBefore(CODE);
}
}
// 生成新的对象替代原有对象
Class<?> newClazz = ctClass.toClass();
return newClazz.newInstance();
} catch (Throwable t) {
LOGGER.error("修改类{}的字节码异常", bean.getClass().getName(), t);
}
}
// 其它对象不做处理
return bean;
}
}
第三种,使用lombok进行自定义扩展。它可以在编译阶段对字节码进行修改
lombok挺好用的,我在上面的User类已经使用它来省略了getter和setter方法。通过自定义扩展,可以让类编译时添加一些代码。
需要引入相关jar包依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
具体怎么操作,我还没研究过。反正我知道它是可以扩展的... ...
文章介绍完了。可能大家会觉得,用第一种Spring的那个Aspect就可以了,后面的搞那么复杂没什么用。哈哈,确实也是,不过是为了学习嘛~
完整项目代码已上传,点击下载