本文介绍SpringBoot条件下,借助于AOP实现自定义注解
所谓元注解,就是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();
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
|
@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"
|
结果如下所示
发送下述请求
1
| curl "127.0.0.1:8080/test/demo1?lastName=Zhu"
|
结果如下所示
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() { ... }
|