日常开发过程中,最常见的异常莫过于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日
*/
@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、可以将切面里面校验到参数(或其部分属性)为空时抛出空指针异常改为返回指定的值(一般是我们封装的通用方法返回值对象)。