微信搜索superit|邀请体验:大数据, 数据管理、OLAP分析与可视化平台 | 赞助作者:赞助作者

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

aop aide_941 42℃

利用自定义注解和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》中提到:

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

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

废话少说,放码过来 。

一、思路

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

二、自定义注解

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

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

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

2、NotNull:作用于字段。

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

三、Aspect切面

敲黑板划重点

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

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

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

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

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

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

  1. /**
  2. * 非空校验的切面
  3. * @author z_hh
  4. * @date 2019年1月2日
  5. */
  6. @Component
  7. @Aspect
  8. public class CheckNullAspect {
  9. private static final ThreadLocal<Info> LOCAL_INFO = new ThreadLocal<Info>() {
  10. protected Info initialValue() {
  11. return new Info();
  12. };
  13. };
  14. // 拦截带@CheckNull的方法
  15. @Pointcut(“@annotation(cn.zhh.null_verify.annotation.CheckNull)”)
  16. private void annotationPointCut() {
  17. }
  18. // 环绕切面
  19. @Around(“annotationPointCut()”)
  20. public Object process(ProceedingJoinPoint pjp) throws Throwable {
  21. // 1、获取目标方法
  22. Signature signature = pjp.getSignature();
  23. MethodSignature methodSignature = (MethodSignature)signature;
  24. Method targetMethod = methodSignature.getMethod();
  25. // 1.1、设置info的类名和方法名
  26. Info info = LOCAL_INFO.get();
  27. info.setClassName(targetMethod.getDeclaringClass().getName());
  28. info.setMethodName(targetMethod.getName());
  29. // 2、获取方法参数和参数值
  30. Parameter[] parameters = targetMethod.getParameters();
  31. Object[] args = pjp.getArgs();
  32. // 3、校验每个参数
  33. for (int i = 0; i < parameters.length; i++) {
  34. Parameter parameter = parameters[i];
  35. // 3.1、获取参数注解
  36. CheckNull annotation = parameter.getAnnotation(CheckNull.class);
  37. // 3.1、不存在@NotNull,忽略
  38. if (Objects.isNull(annotation)) {
  39. continue;
  40. }
  41. // 3.2、校验参数
  42. boolean verify = verifyParameter(annotation.group(), parameter.getName(), args[i]);
  43. if (!verify) {
  44. throw new NullPointerException(LOCAL_INFO.get().toString() + “为空!”);
  45. }
  46. }
  47. // finish、执行目标方法
  48. return pjp.proceed();
  49. }
  50. private boolean verifyParameter(String groupName, String paramName, Object paramValue) throws Exception {
  51. // 1、设置info的参数名
  52. Info info = LOCAL_INFO.get();
  53. info.setParamName(paramName);
  54. // 2、校验参数本身是否为null
  55. if (Objects.isNull(paramValue)) {
  56. return false;
  57. }
  58. // 3、如果参数注解的group属性为””,则无需校验参数属性
  59. if (Objects.equals(groupName, “”)) {
  60. return true;
  61. }
  62. // 4、校验类的字段
  63. Class<?> clazz = paramValue.getClass();
  64. Field[] fields = clazz.getDeclaredFields();
  65. for (Field field : fields) {
  66. NotNull fieldAnnotation = field.getAnnotation(NotNull.class);
  67. // 3.1、没有注解或者注解不包含指定分组
  68. if (Objects.isNull(fieldAnnotation) || !Arrays.asList(fieldAnnotation.groups()).contains(groupName)) {
  69. // 不需要校验
  70. continue;
  71. }
  72. field.setAccessible(true);
  73. // 3.2、获取属性值
  74. Object value = field.get(paramValue);
  75. if (Objects.isNull(value)) {
  76. //获取属性名
  77. String name = field.getName();
  78. info.setFieldName(name);
  79. return false;
  80. }
  81. }
  82. // 5、校验通过
  83. return true;
  84. }
  85. }

你们需要的Info类。

  1. /**
  2. * 参数相关信息
  3. * @author z_hh
  4. * @time 2019年1月2日
  5. */
  6. public class Info {
  7. /** 类名 */
  8. private String className;
  9. /** 方法名 */
  10. private String methodName;
  11. /** 参数名 */
  12. private String paramName;
  13. /** 属性名 */
  14. private String fieldName;
  15. public String getClassName() {
  16. return className;
  17. }
  18. public void setClassName(String className) {
  19. this.className = className;
  20. }
  21. public String getMethodName() {
  22. return methodName;
  23. }
  24. public void setMethodName(String methodName) {
  25. this.methodName = methodName;
  26. }
  27. public String getParamName() {
  28. return paramName;
  29. }
  30. public void setParamName(String paramName) {
  31. this.paramName = paramName;
  32. }
  33. public String getFieldName() {
  34. return fieldName;
  35. }
  36. public void setFieldName(String fieldName) {
  37. this.fieldName = fieldName;
  38. }
  39. @Override
  40. public String toString() {
  41. StringBuilder builder = new StringBuilder();
  42. if (Objects.nonNull(className)) {
  43. builder.append(“类”).append(className);
  44. }
  45. if (Objects.nonNull(methodName)) {
  46. builder.append(“的方法”).append(methodName);
  47. }
  48. if (Objects.nonNull(paramName)) {
  49. builder.append(“的参数”).append(paramName);
  50. }
  51. if (Objects.nonNull(fieldName)) {
  52. builder.append(“的属性”).append(fieldName);
  53. }
  54. return builder.toString();
  55. }
  56. }

四、测试

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

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

  1. /**
  2. * 测试非空校验的服务
  3. * @author z_hh
  4. * @time 2019年1月2日
  5. */
  6. @Service
  7. public class CheckNullService {
  8. @CheckNull
  9. public void test(String nullVal, @CheckNull(group=“test”) Param param) {
  10. System.out.println(param);
  11. }
  12. }

2、自定义参数类。

我们设置了property3非空。

  1. /**
  2. * 自定义参数类
  3. * @author z_hh
  4. * @time 2019年1月2日
  5. */
  6. public class Param {
  7. private int property1;
  8. private String property2;
  9. @NotNull(groups = { “test” })
  10. private Date property3;
  11. @Override
  12. public String toString() {
  13. return “Param [property1=” + property1 + “, property2=” + property2 + “, property3=” + property3 + “]”;
  14. }
  15. /* 省略getter和setter */
  16. }

3、Junit测试代码。

  1. /**
  2. * Junit测试类
  3. * @author z_hh
  4. * @time 2019年1月2日
  5. */
  6. @RunWith(SpringRunner.class)
  7. @SpringBootTest
  8. public class CheckNullServiceTest {
  9. @Autowired
  10. private CheckNullService service;
  11. @Test
  12. public void test() {
  13. Param param = new Param();
  14. service.test(null, param);
  15. }
  16. }

4、运行结果。

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

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

五、优化&扩展

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

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

转载请注明:SuperIT » 利用自定义注解和Aspect实现方法参数的非空校验

喜欢 (0)or分享 (0)