利用自定义注解和Aspect实现方法参数的非空校验

版权声明:本文为博主原创文章,未经允许不得转载。 https://blog.csdn.net/qq_31142553/article/details/85645957

日常开发过程中,最常见的异常莫过于NullPointerException,相信大家都对它恨之入骨吧。我也是。

空指针异常出现的原因有以下几种:

  1. 调用 null 对象的实例方法。

  2. 访问或修改 null 对象的字段。

  3. 如果一个数组为null,试图用属性length获得其长度时。

  4. 如果一个数组为null,试图访问或修改其中某个元素时。

  5. 在需要抛出一个异常对象,而该对象为 null 时。

《dubbo-dev-book.pdf》中提到:

这是我最不喜欢看到的异常,尤其在核心框架中,我更愿看到信息详细的参数不合
法异常。这也是一个健状的程序开发人员,在写每一行代码都应在潜意识中防止
异常。基本上要能确保一次写完的代码,在不测试的情况,都不会出现这两个异常
才算合格。 

方法参数校验又是最频繁的地方,与其在每一个接口的开头写一遍非空校验(如果参数是自定义类型,某些属性可空,某些属性非空,这下就更头疼了),不如将这些重复的工作抽取出来,封装成一个功能组件,这样一劳永逸岂不美哉?!。

废话少说,放码过来 。

扫描二维码关注公众号,回复: 4756490 查看本文章

一、思路

  1. 首先,在需要校验的方法(该类必须为spring bean,后续欢迎改进)加上自定义注解@CheckNull;
  2. 然后,在需要校验的参数前面也加上自定义注解@CheckNull,如果参数为自定义类型且需要对具体字段校验,那么就在方法参数注解里指定group属性,说明此处校验所属的分组名称;
  3. 接着,在自定义类型里面需要校验的字段添加@NotNull注解,并指定groups属性,说明此处校验对哪些分组有效;
  4. 最后,编写Aspect切面对带有@CheckNull注解的方法做拦截、校验。如果不符合,抛出空指针异常,指明某个类某个方法某个参数某个属性为空。

二、自定义注解

我们定义了两个注解,一个作用于方法和参数上,另一个作用于字段上。

1、CheckNull:作用于方法和参数。

/**
 * 设置在不同目标上面有着不同的作用<br>
 * 方法:说明该方法需要校验带该注解参数的非空<br>
 * 参数:说明该参数需要校验非空(自身非空、属性非空)<br>
 * @author z_hh  
 * @date 2019年1月2日
 */
@Documented
@Retention(RUNTIME)
@Target({ METHOD, PARAMETER})
public @interface CheckNull {
	
	/**
	 * 作用于方法和参数上,表面当前校验属于哪一组
	 * 不设置的话,无需校验参数的属性
	 */
	String group() default "";
}

2、NotNull:作用于字段。

/**
 * 设置在不同目标上面有着不同的作用<br>
 * 字段:说明该字段需要校验非空<br>
 * @author z_hh  
 * @date 2019年1月2日
 */
@Documented
@Retention(RUNTIME)
@Target({ FIELD })
public @interface NotNull {
	
	/**
	 * 作用于字段上,表面当前注解对哪一些组有效
	 */
	String[] groups(); 
}

三、Aspect切面

敲黑板划重点

1、定义一个本地线程变量,用于存储校验不通过的类-方法-参数-属性的信息。

2、拦截带@CheckNull注解的方法。

3、分别获取目标方法的参数类数组(Java8提供,相关知识在上一篇博客有所介绍,点击传送)和参数值数组。

4、对包含@CheckNull注解的参数做校验。

5、需要的话对自定义类型的字段校验。

6、校验不通过时,抛出NullPointerException,并说明为空的参数(或其属性)。

/**
 * 非空校验的切面
 * @author z_hh  
 * @date 2019年1月2日
 */
@Component
@Aspect
public class CheckNullAspect {
	
	private static final ThreadLocal<Info> LOCAL_INFO = new ThreadLocal<Info>() {
		protected Info initialValue() {
			return new Info();
		};
	};
    
	// 拦截带@CheckNull的方法
    @Pointcut("@annotation(cn.zhh.null_verify.annotation.CheckNull)")
    private void annotationPointCut() {
    }

    // 环绕切面
    @Around("annotationPointCut()")
    public Object process(ProceedingJoinPoint pjp) throws Throwable {
        
    	// 1、获取目标方法
    	Signature signature = pjp.getSignature();
		MethodSignature methodSignature = (MethodSignature)signature;
		Method targetMethod = methodSignature.getMethod();
		// 1.1、设置info的类名和方法名
		Info info = LOCAL_INFO.get();
		info.setClassName(targetMethod.getDeclaringClass().getName());
		info.setMethodName(targetMethod.getName());
    	
		// 2、获取方法参数和参数值
		Parameter[] parameters = targetMethod.getParameters();
		Object[] args = pjp.getArgs();
		
		// 3、校验每个参数
		for (int i = 0; i < parameters.length; i++) {
			Parameter parameter = parameters[i];
			// 3.1、获取参数注解
			CheckNull annotation = parameter.getAnnotation(CheckNull.class);
			// 3.1、不存在@NotNull,忽略
			if (Objects.isNull(annotation)) {
				continue;
			}
			// 3.2、校验参数
			boolean verify = verifyParameter(annotation.group(), parameter.getName(), args[i]);
			if (!verify) {
				throw new NullPointerException(LOCAL_INFO.get().toString() + "为空!");
			}
		}
		
	    // finish、执行目标方法
	    return pjp.proceed();
    }

	private boolean verifyParameter(String groupName, String paramName, Object paramValue) throws Exception {
		// 1、设置info的参数名
		Info info = LOCAL_INFO.get();
		info.setParamName(paramName);
		// 2、校验参数本身是否为null
		if (Objects.isNull(paramValue)) {
			return false;
		}
		// 3、如果参数注解的group属性为"",则无需校验参数属性
		if (Objects.equals(groupName, "")) {
			return true;
		}
		// 4、校验类的字段
		Class<?> clazz = paramValue.getClass();
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			NotNull fieldAnnotation = field.getAnnotation(NotNull.class);
			// 3.1、没有注解或者注解不包含指定分组
			if (Objects.isNull(fieldAnnotation) || !Arrays.asList(fieldAnnotation.groups()).contains(groupName)) {
				// 不需要校验
				continue;
			}
			field.setAccessible(true);
			// 3.2、获取属性值
			Object value = field.get(paramValue);
			if (Objects.isNull(value)) {
				//获取属性名
				String name = field.getName();
				info.setFieldName(name);
				return false;
			}
		}
		// 5、校验通过
		return true;
	}
}

你们需要的Info类。

/**
 * 参数相关信息
 * @author z_hh
 * @time 2019年1月2日
 */
public class Info {
	
	/** 类名 */
	private String className;
	
	/** 方法名 */
	private String methodName;
	
	/** 参数名 */
	private String paramName;
	
	/** 属性名 */
	private String fieldName;

	public String getClassName() {
		return className;
	}

	public void setClassName(String className) {
		this.className = className;
	}

	public String getMethodName() {
		return methodName;
	}

	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}

	public String getParamName() {
		return paramName;
	}

	public void setParamName(String paramName) {
		this.paramName = paramName;
	}

	public String getFieldName() {
		return fieldName;
	}

	public void setFieldName(String fieldName) {
		this.fieldName = fieldName;
	}
	
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		if (Objects.nonNull(className)) {
			builder.append("类").append(className);
		}
		if (Objects.nonNull(methodName)) {
			builder.append("的方法").append(methodName);
		}
		if (Objects.nonNull(paramName)) {
			builder.append("的参数").append(paramName);
		}
		if (Objects.nonNull(fieldName)) {
			builder.append("的属性").append(fieldName);
		}
		
		return builder.toString();
	}

}

四、测试

写完一个功能后最开心的时刻。

1、使用该功能的目标方法。

/**
 * 测试非空校验的服务
 * @author z_hh
 * @time 2019年1月2日
 */
@Service
public class CheckNullService {

	@CheckNull
	public void test(String nullVal, @CheckNull(group="test") Param param) {
		System.out.println(param);
	}
}

2、自定义参数类。

我们设置了property3非空。

/**
 * 自定义参数类
 * @author z_hh
 * @time 2019年1月2日
 */
public class Param {

	private int property1;
	
	private String property2;
	
	@NotNull(groups = { "test" })
	private Date property3;

	@Override
	public String toString() {
		return "Param [property1=" + property1 + ", property2=" + property2 + ", property3=" + property3 + "]";
	}
	
	/* 省略getter和setter */
	
}

3、Junit测试代码。 

/**
 * Junit测试类
 * @author z_hh
 * @time 2019年1月2日
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class CheckNullServiceTest {

	@Autowired
	private CheckNullService service;
	
	@Test
	public void test() {
		
		Param param = new Param();
		service.test(null, param);
		
	}
}

4、运行结果。

哎呀,报错了???不对,这不就是我们期望的结果吗?!

※如果看到property3变成了arg1,说明你的开发环境没有配置开启-parameters(怎么弄?我教你,点击传送)。

五、优化&扩展

1、可以在@CheckNull里面定义一个message属性,作用于方法参数上时指定相应的值,然后在切面里面校验到参数(或其部分属性)为空时,获取注解的该属性值,取代默认的"类-方法-参数-属性为空!"的信息。

2、可以将切面里面校验到参数(或其部分属性)为空时抛出空指针异常改为返回指定的值(一般是我们封装的通用方法返回值对象)。

猜你喜欢

转载自blog.csdn.net/qq_31142553/article/details/85645957
今日推荐