利用自定义注解和Aspect实现方法参数的非空校验
日常开发过程中,最常见的异常莫过于NullPointerException,相信大家都对它恨之入骨吧。我也是。
空指针异常出现的原因有以下几种:
- 调用 null 对象的实例方法。
- 访问或修改 null 对象的字段。
- 如果一个数组为null,试图用属性length获得其长度时。
- 如果一个数组为null,试图访问或修改其中某个元素时。
- 在需要抛出一个异常对象,而该对象为 null 时。
《dubbo-dev-book.pdf》中提到:
这是我最不喜欢看到的异常,尤其在核心框架中,我更愿看到信息详细的参数不合法异常。这也是一个健状的程序开发人员,在写每一行代码都应在潜意识中防止的异常。基本上要能确保一次写完的代码,在不测试的情况,都不会出现这两个异常才算合格。
方法参数校验又是最频繁的地方,与其在每一个接口的开头写一遍非空校验(如果参数是自定义类型,某些属性可空,某些属性非空,这下就更头疼了),不如将这些重复的工作抽取出来,封装成一个功能组件,这样一劳永逸岂不美哉?!。
废话少说,放码过来 。
一、思路
- 首先,在需要校验的方法(该类必须为spring bean,后续欢迎改进)加上自定义注解@CheckNull;
- 然后,在需要校验的参数前面也加上自定义注解@CheckNull,如果参数为自定义类型且需要对具体字段校验,那么就在方法参数注解里指定group属性,说明此处校验所属的分组名称;
- 接着,在自定义类型里面需要校验的字段添加@NotNull注解,并指定groups属性,说明此处校验对哪些分组有效;
- 最后,编写Aspect切面对带有@CheckNull注解的方法做拦截、校验。如果不符合,抛出空指针异常,指明某个类某个方法某个参数某个属性为空。
二、自定义注解
我们定义了两个注解,一个作用于方法和参数上,另一个作用于字段上。
1、CheckNull:作用于方法和参数。
-
/**
-
* 设置在不同目标上面有着不同的作用<br>
-
* 方法:说明该方法需要校验带该注解参数的非空<br>
-
* 参数:说明该参数需要校验非空(自身非空、属性非空)<br>
-
* @author z_hh
-
* @date 2019年1月2日
-
*/
-
-
(RUNTIME)
-
({ METHOD, PARAMETER})
-
public CheckNull {
-
-
/**
-
* 作用于方法和参数上,表面当前校验属于哪一组
-
* 不设置的话,无需校验参数的属性
-
*/
-
String group() default “”;
-
}
2、NotNull:作用于字段。
-
/**
-
* 设置在不同目标上面有着不同的作用<br>
-
* 字段:说明该字段需要校验非空<br>
-
* @author z_hh
-
* @date 2019年1月2日
-
*/
-
-
(RUNTIME)
-
({ FIELD })
-
public NotNull {
-
-
/**
-
* 作用于字段上,表面当前注解对哪一些组有效
-
*/
-
String[] groups();
-
}
三、Aspect切面
敲黑板划重点
1、定义一个本地线程变量,用于存储校验不通过的类-方法-参数-属性的信息。
2、拦截带@CheckNull注解的方法。
3、分别获取目标方法的参数类数组(Java8提供,相关知识在上一篇博客有所介绍,点击传送)和参数值数组。
4、对包含@CheckNull注解的参数做校验。
5、需要的话对自定义类型的字段校验。
6、校验不通过时,抛出NullPointerException,并说明为空的参数(或其属性)。
-
/**
-
* 非空校验的切面
-
* @author z_hh
-
* @date 2019年1月2日
-
*/
-
-
-
public class CheckNullAspect {
-
-
private static final ThreadLocal<Info> LOCAL_INFO = new ThreadLocal<Info>() {
-
protected Info initialValue() {
-
return new Info();
-
};
-
};
-
-
// 拦截带@CheckNull的方法
-
“@annotation(cn.zhh.null_verify.annotation.CheckNull)”)(
-
private void annotationPointCut() {
-
}
-
-
// 环绕切面
-
“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;
-
}
-
-
-
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日
-
*/
-
-
public class CheckNullService {
-
-
-
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;
-
-
“test” })(groups = {
-
private Date property3;
-
-
-
public String toString() {
-
return “Param [property1=” + property1 + “, property2=” + property2 + “, property3=” + property3 + “]”;
-
}
-
-
/* 省略getter和setter */
-
-
}
3、Junit测试代码。
-
/**
-
* Junit测试类
-
* @author z_hh
-
* @time 2019年1月2日
-
*/
-
(SpringRunner.class)
-
-
public class CheckNullServiceTest {
-
-
-
private CheckNullService service;
-
-
-
public void test() {
-
-
Param param = new Param();
-
service.test(null, param);
-
-
}
-
}
4、运行结果。
哎呀,报错了???不对,这不就是我们期望的结果吗?!
※如果看到property3变成了arg1,说明你的开发环境没有配置开启-parameters(怎么弄?我教你,点击传送)。
五、优化&扩展
1、可以在@CheckNull里面定义一个message属性,作用于方法参数上时指定相应的值,然后在切面里面校验到参数(或其部分属性)为空时,获取注解的该属性值,取代默认的”类-方法-参数-属性为空!”的信息。
2、可以将切面里面校验到参数(或其部分属性)为空时抛出空指针异常改为返回指定的值(一般是我们封装的通用方法返回值对象)。
转载请注明:SuperIT » 利用自定义注解和Aspect实现方法参数的非空校验