0%

基于SpringBoot AOP条件下自定义、实现注解

本文介绍SpringBoot条件下,借助于AOP实现自定义注解

abstract.jpg

Meta Annotation元注解

所谓元注解,就是Java提供的、负责修饰其他注解的注解。常见地有:

@Target注解

其定义了注解可以作用的位置,其value属性地常用取值有:

  • ElementType.PACKAGE:包
  • ElementType.TYPE:类、接口、枚举
  • ElementType.FIELD:字段
  • ElementType.METHOD:方法
  • ElementType.PARAMETER:方法形参
  • ElementType.CONSTRUCTOR:构造器方法

@Target注解的value是数组类型,当只有一个元素时,可以省略数组写法。示例如下所示

1
2
3
4
5
6
7
// 该注解可用于方法上
@Target(ElementType.METHOD)

// 该注解可用于字段、方法、构造器方法上
@Target({
ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR
})

@Retention注解

其定义了注解的生命周期。其value属性地常用取值有:

  • RetentionPolicy.SOURCE:在Java源文件中有效。编译器会丢弃掉
  • RetentionPolicy.CLASS:在Class文件中有效。运行时JVM会丢弃掉
  • RetentionPolicy.RUNTIME:运行时有效。此时即可通过反射获取到该注解

日常开发中,对于@Retention注解而言。我们用的更多的就是RetentionPolicy.RUNTIME了。示例如下

1
2
// 该注解保留到运行时
@Retention(RetentionPolicy.RUNTIME)

@Repeatable注解

默认情况下,注解不可以在同一处重复使用。为此Java 8中引入了@Repeatable注解解决该问题。通过添加@Repeatable注解表示@Family注解可在同一处重复使用。同时,我们需要在@Repeatable注解的值中指定另一个注解@Families。表示可以通过@Families注解的值来包含这个可重复的注解Family。显然此时,@Families中value属性的类型则必须是@Family注解的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target(ElementType.FIELD) // 该注解用于字段
@Retention(RetentionPolicy.RUNTIME) // 该注解保留到运行时
@Repeatable(Families.class) // 该注解可重复
public @interface Family {
String value() default "";
}

...

@Target(ElementType.FIELD) // 该注解用于字段
@Retention(RetentionPolicy.RUNTIME) // 该注解保留到运行时
public @interface Families {
Family[] value();
}

现在,我们就可以在同一处重复使用@Family注解了。下述两种写法均可

1
2
3
4
5
6
7
8
9
10
public class User {
@Family("Aaron")
@Family("Bob")
private int name1;

@Families({
@Family("Aaron"), @Family("Bob")
})
private String name2;
}

自定义注解

自定义注解的基本语法格式如下例所示。其中自定义注解可通过下述形式定义注解属性。可通过default指定属性的默认值,如果不指定默认值,则在使用注解时必须显式设置属性值,而无法使用默认值。需注意属性类型仅限下述几种:

  • 基本数据类型(boolean, byte, char, short, int, long, float, double)
  • String类型、Class类型、注解类型、枚举类
  • 上述类型的数组
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 自定义注解
*/
@Target(ElementType.METHOD) // 该注解用于方法
@Retention(RetentionPolicy.RUNTIME) // 该注解保留到运行时
public @interface MyLog {
// 定义类型为long、名为value的属性
long value();

// 定义类型为String、名为level的属性, 默认值为 "INFO"
String level() default "INFO";
}

基于AOP实现注解

完成自定义注解后,我们期望在方法上添加注解,能够在调用方法的前后实现日志输出(包含方法入参、方法结果等信息)。这里我们结合SpringBoot的AOP来实现对自定义注解输出日志的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 实现@Mylog注解功能的切面类
*/
@Component
@Aspect
@Slf4j
public class MyLogAop {

@Around( "@annotation(com.aaron.SpringBoot1.annotation.MyLog)" )
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法信息
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
String methodName = methodSignature.getName(); // 获取方法名
String[] paramNames = methodSignature.getParameterNames(); // 获取方法参数名
Object[] args = joinPoint.getArgs(); // 获取方法参数值

// 获取注解信息
MyLog myLog = methodSignature.getMethod().getAnnotation( MyLog.class);
long timestamp = myLog.value();
String level = myLog.level();

StringBuilder sb = new StringBuilder();
for (int i=0; i<paramNames.length; i++) {
sb.append("<").append(paramNames[i]).append(":").append(args[i]).append(">");
}

// 执行开始前打印日志
String startMsg = "[START] TS: "+ timestamp + " <"+level+">" + " MethodName: " + methodName + " Param: " + sb.toString();
log.info(startMsg);

// 调用目标方法
Object res="Exec Failed";
try {
res = joinPoint.proceed();
} catch (Exception e) {
log.error("Happen Excep: {}", e.getMessage());
}

// 执行完成后打印日志
String endMsg = "[END] TS: "+ timestamp + " <"+level+">" + " MethodName: " + methodName + " Result: " + res;
log.info(endMsg);
return res;
}
}

至此,就可以使用该注解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("test")
public class TestController {

@MyLog(1995832)
@GetMapping("/demo1")
public String test1(@RequestParam(required = false) String firstName, @RequestParam(required = false) String lastName) {
String res = "Hello";
if( firstName!=null ) {
res += " " + firstName;
}
if( lastName!=null ) {
res += " " + lastName;
}
return res;
}
}

发送下述请求

1
curl "127.0.0.1:8080/test/demo1?firstName=Tony&lastName=Wang"

结果如下所示

figure 1.jpg

发送下述请求

1
curl "127.0.0.1:8080/test/demo1?lastName=Zhu"

结果如下所示

figure 2.jpg

Note

如果一个注解中有一个名为value的属性。在使用该注解时,如果只设置value属性的话(要么该注解中只有一个value属性、要么其他属性均使用默认值),则可以省略掉属性名value。如下所示

1
2
3
4
5
6
7
8
9
10
public class Task {    
@MyLog(22)
public void runTask1() {
...
}

@MyLog(value = 22)
public void runTask2() {
...
}
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝