前言
本文涉及AOP以及自定义注解两个功能点。切面部分,涉及的一些基本的切面知识点,同时也包含一些反射的相关内容。自定义注解没什么好说的,注意元注解的使用方法以及参数使用就好。
早在几个月前就写好了AOP专题和注解的相关知识点题目草稿,不过由于前几个月加班严重,所以一直没时间。蹭着试运行这段的空闲期做个总结。
实现
大体思路
可以先参考@Valid的校验流程:
- 参数如果被@Valid注解,那么就对该参数进行校验。这是校验前提。
@PostMapping("/example")
public void example(@RequestBody @Valid MeetingDTO meetingDTO ) {
}
- 参数对象中的属性如果被@NotEmpty @NotNull @Max @Min等注解注解,标明该属性需要做非空校验、非null校验、最大最小值校验等等。这是具体的校验规则。
@Data
public class MeetingDTO implements Serializable {
@NotEmpty(message = "预约id不能为空")
private String orderId;
@NotEmpty(message = "预约状态不能为空")
private String orderStatus;
}
- 当校验不通过时,即上述校验结果为空或null时抛出异常[org.springframework.web.bind.MethodArgumentNotValidException];否则正常运行。这是最终校验的结果。
我们可以根据如上三个要点入手,用相同的方法实现我们自定义注解的校验功能。
要点
根据@Valid的思路与具体实现,要考虑到如下几点:
-
为了多种校验规则能够实现,可以添加一个父注解,方便后续管理及实现。
-
切面可以拦截使用注解的方法,但是没法拦截使用注解的参数。因此无法向@Valid那样直接作用到参数上,所以我们可以退一步,直接加到方法级别上(这里可以使用Java的拦截器拦截指定接口请求,然后校验参数中是否有注解,但是我暂时还没使用过拦截器与注解的搭配,就不献丑了)。
// 无法拦截
@PostMapping("/valid")
public String valid(@RequestBody @CValid ExampleDTO exampleDTO) {
}
// 可以
@CValid
@PostMapping("/valid")
public String valid(@RequestBody ExampleDTO exampleDTO) {
}
OK,可以正式开始敲代码了。
自定义注解
- 首先实现检验参数的注解。该注解作用于方法。其功能是标记需要做数据校验的方法参数。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface CValid {
}
2. 所有具体校验规则注解的父类注解。类似于**接口**的作用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, ANNOTATION_TYPE})
@Inherited
public @interface ValidMark {
}
3. 实现一个**非空校验**的注解,该注解用于字段或参数。message参数用于报错消息展示。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, PARAMETER })
@ValidMark // 注解标记
public @interface CNotEmpty {
/**
* 报错消息
* @return
*/
public String message() default "字段不能为null或空";
}
##### 异常类 自定义异常类。
public class CValidException extends RuntimeException {
public CValidException(String message) {
super("com.training.spring.exception.CValidException: field valid failed: "
+ message);
}
public CValidException() {
super();
}
}
AOP实现注解功能
切面功能的代码实现思路如下:
-
设置目标切点,获取参数数据、注解数据。
-
根据上述数据判断类型:是对象校验还是对象属性校验 、是非空校验还是其他校验规则。
-
最后根据校验规则,判断是否抛出异常。
代码如下:
@Aspect
@Component
public class ValidAspect {
@Pointcut("@annotation(com.training.spring.aop.annotation.valid.CValid)")
public void check() {
}
@Before("check()")
public void beforeCheck(JoinPoint joinpoint) {
// 获取参数注解
MethodSignature signature = (MethodSignature) joinpoint.getSignature();
Method method = signature.getMethod();
Annotation[][] annotations = method.getParameterAnnotations();
// 获取入参
Object[] args = joinpoint.getArgs();
for (int i = 0; i < args.length; i++) {
Object object = args[i];
Class<?> aClass = object.getClass();
Annotation[] paraAnnotation = annotations[i];
Annotation annotation = checkAnnotationExist(paraAnnotation);
if (null != annotation) {
// 验证当前对象
this.validObject(object, aClass, annotation);
} else {
// 验证对象的属性
this.validObjectField(object, aClass);
}
}
}
// 判断是否是校验注解
public Annotation checkAnnotationExist(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType().isAnnotationPresent(ValidMark.class))
return annotation;
}
return null;
}
// 校验对象
public void validObject(Object object, Class<?> aClass, Annotation annotation) {
// 校验是否为空
if (CNotEmpty.class == annotation.annotationType()) {
if (this.checkNullOrBlank(object, aClass)) {
throw new CValidException(((CNotEmpty) annotation).message());
}
}
// 校验其他…………
}
// 校验属性
public void validField(Field field) {
field.setAccessible(true);
// 校验是否为空
if (field.isAnnotationPresent(CNotEmpty.class)) {
if (this.checkNullOrBlank(field, field.getClass())) {
CNotEmpty cNotEmpty = field.getAnnotation(CNotEmpty.class);
String message = cNotEmpty.message();
throw new CValidException(message);
}
}
// 校验其他…………
}
// 校验对象的属性
public void validObjectField(Object object, Class<?> aClass) {
Field[] fields = aClass.getDeclaredFields();
if (fields.length < 1)
return;
for (Field field : fields) {
// 校验属性
this.validField(field);
}
}
// 验证参数是否为空
public boolean checkNullOrBlank(Object object, Class<?> aClass) {
if (null == object)
return true;
/**
* Boolean Character Byte Short Integer Long Float Double Void
*/
if (aClass.isPrimitive())
return false;
if (object instanceof String)
return StringUtils.isEmpty(object);
if (object instanceof Collection)
return CollectionUtils.isEmpty((Collection<?>) object);
return false;
}
}
调用
controller层调用
// 作用于属性
@CValid
@PostMapping("/object")
public String object(@RequestBody ExampleDTO exampleDTO) {
System.out.println(exampleDTO.toString());
return exampleDTO.getName();
}
// 作用于参数
@CValid
@PostMapping("/field")
public String field(@RequestBody @CNotEmpty List<String> list) {
System.out.println(list.toString());
return null;
}
对象类展示
public class ExampleDTO implements Serializable {
@CNotEmpty(message = "姓名不能为空")
private String name;
}
总结
至此整体功能实现,框架也有了雏形。不过缺陷还是存在的,校验注解的实现,依赖于if else编程,容易造成代码冗余,难以维护;除此之外还有很多功能缺失。需要继续学习相关知识才行。此外多看英文文档有好处,或许能让你对概念的理解更深刻。
参考
Lesson: Annotations.
Aspect Oriented Programming with Spring.
Overview of Java Built-in Annotations.
Creating a Custom Annotation in Java.
Spring AOP Tutorial.