利用Aspect/Javassist/动态代理/Lombok等方式省略Controller的参数校验结果处理代码

版权声明:欢迎转载,记得前排留名哦☞ https://blog.csdn.net/qq_31142553/article/details/86547201

开发过程中,后台的参数校验是必不可少的,所以经常会看到类似下面这样的代码

这样写并没有什么错,还挺工整的,只是看起来不是很优雅而已。

那么有什么办法可以省去这么繁琐的工作呢?

当然,利用自定义注解和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、测试一下

使用MockMvc测试API

可以看到,将不通过的校验的信息打印出来了 

这样,就算是解决了参数校验的问题,一切似乎很完美... ...

但是,如果我们每个需要校验的方法代码里面都这样写,是否觉得不太妥?

我觉得有两个问题,第一是代码重复了,第二是跟业务逻辑耦合了。

那有什么方法解决呢?

或许可以将这段代码封装到一个函数,放到一个公共类(或者父类)里面。但是,还是要在每个方法体里面写代码,只是少了一些而已,不算好的方法。

直接敲黑板划重点吧

二、省略方法体内处理校验结果代码的几种方式

第一种,也是我觉得最简单的,就是使用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就可以了,后面的搞那么复杂没什么用。哈哈,确实也是,不过是为了学习嘛~

完整项目代码已上传,点击下载

猜你喜欢

转载自blog.csdn.net/qq_31142553/article/details/86547201