SpringAOP 结合 Java 反射机制为指定注解提供校验能力-针对一般接口入参校验场景的解决方案

版权声明:欢迎转载交流,声明出处即可。体能状态先于精神状态,习惯先于决心,聚焦先于喜好 ——Bestcxx https://blog.csdn.net/bestcxx/article/details/91041770

前言

体能状态先于精神状态,习惯先于决心,聚焦先于喜好

入参校验的必要性

属性校验在实际开发中是一个非常常见的场景,尤其在web服务和对外提供接口服务时,对入参对合法性校验是确保程序安全的第一道关卡.

校验注解

Spring Validation 验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种)
javax提供了@Valid(标准JSR-303规范)
二者配合BindingResult可以直接提供参数验证结果。
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。

常见的校验相关注解和含义

需要强调的是,注解只是一种标注,其自身除了携带一些基本信息外,无法自动完成校验功能,一般需要借助于相应的处理逻辑

  • @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
  • @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
  • @NotEmpty(message=“提示信息”):用在集合类上面;不能为null,而且长度必须大于0
  • @NotBlank(message=“提示信息”): 用在String上面;只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
  • @NotNull(message=“提示信息”):用在基本类型上;不能为null,但可以为empty
  • @Length(min=, max=,message="") : 只适用于String 类型
  • @Size(min=,max=,message=""):只适用于整形
  • @AssertTrue: 验证 Boolean 对象是否为 true
  • @AssertFalse: 验证 Boolean 对象是否为 false
  • @Past: 验证 Date 和 Calendar 对象是否在当前时间之前
  • @Future: 验证 Date 和 Calendar 对象是否在当前时间之后
  • @Pattern: 验证 String 对象是否符合正则表达式的规则
  • @Min: 验证 Number 和 String 对象是否大等于指定的值
  • @Max: 验证 Number 和 String 对象是否小等于指定的值
  • @DecimalMax: 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
  • @DecimalMin: 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
  • @Digits: 验证 Number 和 String 的构成是否合法
  • @Digits(integer=,fraction=): 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。

常规校验的两种方式及不足

Web 应用与 @Validated、@Valid 、BindingResult

在 Web 应用中我们可以选择使用这种方式对入参进行校验.

  • 案例代码

Person 为实体类,请参考本文末代码段 Person.java

@RequestMapping("/demo")
public String demo(@Validated Person person,BindingResult result) {
	if(result.hasErrors()){  
           List<ObjectError> ls=result.getAllErrors();  
           for (int i = 0; i < ls.size(); i++) {  
               System.out.println("error:"+ls.get(i));  
               return "error";
           }  
       }
	return "success";
}
手动校验字段合法性

另一种方法是手动对入参进行校验,这个就比较灵活类,比如为入参提供一个单独对校验方法,入下面的代码

  • 案例代码
/**
 * 校验 Person 字段的格式合法性
 * @param person
 * @return
 */
private boolean checkPerson(Person person) {
	if(StringUtils.isBlank(person.getName())){
		return false;
	}
	return true;
}
两种方式的不足

WEB 应用的校验方式被局限在 WEB应用中了,第二种手动处理的方式则无法统一处理校验逻辑,并且没有应用注解——显而易见的规则描述.

多想一步:借助注解自己写校验的逻辑

再次强调,注解虽然可以携带信息,但是注解本身并不能自动完成校验功能.

思路阐述

1、利用 Spring AOP 将需要进行校验的方法设置为切点
2、使用 Java 反射机制,检验入参的成员变量是否有被注解修饰
3、对于存在注解修饰的成员变量的值和注解的规则是否匹配
4、对于满足校验条件的不做处理,返回正常的状态信息,对于不满足条件的,抛出异常
5、对于异常进行统一处理,返回错误状态信息

代码 UML 设计

实体类 Person.java
实体类:ResponseBody.java,用于接口方法统一的返回值-便于正常情况和异常情况的统一处理
接口:TemService
类:TemServiceImpl:TemService的实现类
类:MyAspect,切面定义类,对 TemService 的 sayHello 方法设置为切点
测试类:AspectTest
配置文件:applicationContext-tem.xml

在这里插入图片描述

关门,放代码
Person.java
package com.bestcxx.stu.tem.aspect.dto;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
 * 员工类
 * @author wujie
 */
public class Person {
	/**姓名*/
	@NotNull(message = "name不可为空")
	private String name;
	/**年龄*/
	@Size(min = 1,max=200,message = "age应在1到200之间")
	private int age;
	/**备注*/
	private String comments;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	
	public String getComments() {
		return comments;
	}
	public void setComments(String comments) {
		this.comments = comments;
	}
	@Override
	public String toString() {
		/*
		 * 查看对方包的 maven依赖:https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.9
		 **/
		return ToStringBuilder.reflectionToString(this);
	}
}

ResponseBody.java
package com.bestcxx.stu.tem.aspect.dto;

import java.io.Serializable;

import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * 承接结果的实体
 * @author wujie
 *
 */
public class ResponseBody implements Serializable{
	private static final long serialVersionUID = 1L;
	private String code;
	private String msg;
	public String getCode() {
		return code;
	}
	public void setCode(String code) {
		this.code = code;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}
	
	@Override
	public String toString() {
		/*
		 * 查看对方包的 maven依赖:https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.9
		 **/
		return ToStringBuilder.reflectionToString(this);
	}
}

TemService.java
package com.bestcxx.stu.tem.aspect.service;

import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;

/**
 * 定义接口
 * @author wujie
 */
public interface TemService {
	ResponseBody sayHello(Person p);
}

TemServiceImpl.java
package com.bestcxx.stu.tem.aspect.serviceimpl;

import org.springframework.stereotype.Component;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
import com.bestcxx.stu.tem.aspect.service.TemService;

@Component
public class TemServiceImpl implements TemService {

	@Override
	public ResponseBody sayHello(Person p) {
		System.out.println("进入 sayHello");
		ResponseBody r=new ResponseBody();
		r.setCode("SUCCESS");
		r.setMsg("调用成功");
		return r;
	}
}

MyAspect.java
package com.bestcxx.stu.tem.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;

/**
 * 自定义切面对指定切点对入参的成员变量注解进行合规校验
 * 切点对应的方法有全局返回值-成功/失败是同一个类的对象,状态码区分成功或失败
 * @author wujie
 */
@Aspect
@Component
public class MyAspect {
	
	/**
	 * 定义切点
	 * @param point
	 * @return
	 */
	@Pointcut("execution(* com.bestcxx.stu.tem.aspect.service.TemService.sayHello(..))")
	public void point() {
	}
	
	/**
	 * 切面的处理方法
	 * @param point
	 * @return
	 */
	@Around(value="point()")
	public Object around(ProceedingJoinPoint point) {
		System.out.println("来自@aspect的@Around注解的通知:这是一个切点");
		try {
			//获取第一个参数
			//本切面拦截的方法只有一个入参 Person
			Object object0=point.getArgs()[0];
			
			//本例子作为测试方法,限定了对 Person类 的校验,如果需要通用方法可以去除本if方法
			if(object0 instanceof Person) {
				//方法内部对 object0 对应对注解和成员变量值对约定进行匹配
				checkFieldsByObject(object0);
			}
			//正常逻辑不受影响
			Object result = point.proceed();
			return result;
		}catch(Throwable t) {
			//如果上面的校验抛出了异常,在这里进行捕获
			ResponseBody r=new ResponseBody();
			r.setCode("ERROR");
			r.setMsg(t.getMessage());
			return r;
		}
	}
	/**
	 * 检测一个 Object 对象的注解和值是否匹配
	 * @param object0 被检测的对象
	 * @throws Exception
	 */
	private void checkFieldsByObject(Object object0) throws Exception{
		//获取该类的成员变量
		Field[] fields=Person.class.getDeclaredFields();
		//对成员变量进行遍历
		for(Field f:fields) {
			//指定对象的 指定成员方法 检测 Field
			checkAnnotationByField(object0,f);
		}
	}
	
	/**
	 * @param object0  被检测的对象
	 * @param f  Field 对应的成员变量
	 * @throws Exception
	 */
	private void checkAnnotationByField(Object object0,Field f) throws Exception {
		//需要考虑一个属性同时被多个注解修饰到情况
		Annotation[] annotations=f.getAnnotations();
		for(Annotation annotation:annotations) {
			//指定对象、注解、成员变量的具体检测
			checkFiledValueByAnnotationType(object0,annotation,f);
		}
	}
	
	/**
	 * 具体的不同的注解采取不同的处理逻辑
	 * @param object0     被检测的对象
	 * @param annotation  对应的注解类别
	 * @param f  Field    对应的成员变量     
	 * @throws Exception
	 */
	private void checkFiledValueByAnnotationType(Object object0,Annotation annotation,Field f)  throws Exception {
		//@NotNull 注解判断
		if(annotation instanceof NotNull) {
			//允许操作 private 修饰的成员变量
			f.setAccessible(true);
			//判断该入参的属性是否为 null
			if(f.get(object0)==null) {
				throw new Exception(((NotNull) annotation).message());
			}
		}
		
		//@Size 注解判断
		if(annotation instanceof Size) {
			//允许操作 private 修饰的成员变量
			f.setAccessible(true);
			if(((Size) annotation).min()>f.getInt(object0)||((Size) annotation).max()<f.getInt(object0)){
				throw new Exception(((Size) annotation).message());
			}
		}
	}
}

applicationContext-tem.xml

务必注意这里需要 aop:aspectj-autoproxy/,否则 Spring AOP 功能无法奏效

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"    
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     
    xmlns:context="http://www.springframework.org/schema/context"    
    xmlns:tx="http://www.springframework.org/schema/tx"    
    xmlns:aop="http://www.springframework.org/schema/aop"    
    xmlns:util="http://www.springframework.org/schema/util"    
    xmlns:orm="http://www.springframework.org/schema/orm"     
    xmlns:mvc="http://www.springframework.org/schema/mvc"    
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd    
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd    
                        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd    
                        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd    
                        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd    
                        http://www.springframework.org/schema/orm http://www.springframework.org/schema/orm/spring-orm-4.3.xsd    
                        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd    
    "
    >  
    <!--开启AOP功能-->
	<aop:aspectj-autoproxy/>
	<!--启动bean扫描注册和注解注入-->
    <context:component-scan base-package="com.bestcxx.stu.tem.aspect"/>
    
</beans>
  
测试类 AspectTest.java
package com.bestcxx.stu.tem.aspect;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
import com.bestcxx.stu.tem.aspect.service.TemService;

@DirtiesContext
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:test/applicationcontext-tem.xml"})
//@TransactionConfiguration(transactionManager = "defaultTransactionManager",defaultRollback=false)//事务管理
//@Rollback(true)
public class AspectTest {
	@Autowired
	private TemService temService;
	
	@Test
	public void testSayHello() {
		Person p=new Person();
		//p.setAge(220);
		p.setName("wj");
		p.setComments("SUCCESS 校验通过,ERROR校验不通过");
		ResponseBody rb=temService.sayHello(p);
		System.out.println("接口返回结果:"+rb.toString());
	}
}

  • 校验通过的结果
来自@aspect的@Around注解的通知:这是一个切点
进入 sayHello
接口返回结果:com.bestcxx.stu.tem.aspect.dto.ResponseBody@73e9cf30[code=SUCCESS,msg=调用成功]

  • 校验不通过的结果
来自@aspect的@Around注解的通知:这是一个切点
接口返回结果:com.bestcxx.stu.tem.aspect.dto.ResponseBody@73e9cf30[code=ERROR,msg=age应在1到200之间]

jdk代理还是cglib代理?

在xml文档中,我们设置了开启了AOP的内容
<aop:aspectj-autoproxy/>这种情况下,Spring 在使用切面功能时,如果切面是一个类,并且这个类实现类接口,那么Spring会使用jdk动态代理进行处理.如果这个类没有实现接口,Spring就会使用cglib进行动态代理.
这里隐藏着一个问题,就是一旦一个类实现类接口,并且开启AOP功能,但是在声明的时候以类进行声明应用,那么在<aop:aspectj-autoproxy/>情况下,会报错,说找不到这个类
这个时候需要手动开启cglib代理 <aop:aspectj-autoproxy proxy-target-class="true"/>
jdk动态代理和cglib动态代理的区别在于jdk只能代理接口,cglib可以代理类

参考连接

[1]、https://www.cnblogs.com/thinkjava/p/7073599.html
[2]、https://www.jianshu.com/p/9e33ec934ff0
[3]、https://blog.csdn.net/bestcxx/article/details/79145859
[4]、https://blog.csdn.net/bestcxx/article/details/88645684
[5]、https://blog.csdn.net/bestcxx/article/details/52826058

猜你喜欢

转载自blog.csdn.net/bestcxx/article/details/91041770
今日推荐