自定义注解

元注解

在 Spring 中,我们经常能够看到各种各样的注解,Java 自身也定义了很多的注解,这些注解的添加能够让程序员明确的知道这个类的状态。

Java中比较常见的注解类:

  • @Override 重写父类的方法
  • @Deprecated 标记过时,不建议再使用
  • @SuppressWarnings 消除警告

Java中的注解类,都使用 @interface 标记

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

在 Override 注解类中,可以看到有两个注解 @Targe t和 @Retention 对 Override 进行了修饰,这些用来修饰注解类的注解称为元注解,元注解是注解的注解。

Java 中的元注解主要有以下几种

  • Target
  • Retention
  • Documented
  • Inherited

Target

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}

可以看到,Target 有一个属性 value,类型是 ElementType 数组,ElementType 是一个枚举类型,主要用来标记被@Target修饰的注解类可应用在什么位置上

枚举值为

  • ANNOTATION_TYPE 可应用在注解类上
  • CONSTRUCTOR 构造器
  • FIELD 成员变量
  • LOCAL_VARIABLE 局部变量
  • METHOD 方法
  • PACKAGE 包
  • PARAMETER 参数
  • TYPE 类、接口以及枚举

JDK1.8 之后新增了两个枚举属性

  • TYPE_PARAMETER 类型参数声明
  • TYPE_USE 使用的类型

Retention

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}

Retention 注解有一个属性 value,是 RetentionPolicy 类型的,RetentionPolicy 是一个枚举类型,这个枚举决定了 Retention 注解的保留机制

RetentionPolicy 有3个值:

  • CLASS 表示当程序编译时注解的信息被保留在 class 文件(字节码文件)中,但在运行的时候不会被虚拟机读取
  • RUNTIME 表示当程序编译时注解的信息被保留在 class 文件(字节码文件)中,运行时也会被虚拟机读取
  • SOURCE 表示注解的信息会被编译器抛弃,不会留在 class 文件中,注解的信息只会留在源文件中

Documented

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {

}

@Documented 元注解用于声明被该注解修饰的注解类是可以写入 JavaDoc 中的

Inherited

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Inherited 用于指定某个注解用于父类时是否能够被子类继承

@Inherited 使用的较少,稍微理解即可

自定义注解

下面以日志注解为例介绍如何自定义注解。

首先需定义注解类 MyLog

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {

}

@Target(ElementType.METHOD) 表明该注解只能在方法上使用

@Retention(RetentionPolicy.RUNTIME) 表明该注解会在程序编译时放到 class 文件(字节码文件)中,运行时也会被虚拟机读取

@Documented 声明被该注解修饰的注解类是可以写入 JavaDoc 中

@Target 和 @Retention 是必须的,@Documented 根据自己的需要进行添加

此时,只是定义了MyLog注解,但是并不是加了该注解就起作用的,还需要配置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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Aspect
@Component
@Slf4j
public class MyLogAspect {

@Pointcut("@annotation(com.example.demo.annotation.MyLog)")
public void myLog() {

}

/**
* 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】
* @param joinPoint
*/
@Before("myLog()")
public void before(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】");
}
}

/**
* 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】
* 环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行
* @param joinPoint
*/
@Around("myLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取请求相关信息
log.debug("URL ------> " + request.getRequestURL().toString());
log.debug("METHOD ----> " + request.getMethod());
log.debug("ContentType ----->" + request.getContentType());
log.debug("Parameters ----->" + request.getParameterMap());

//获取切入点的相关信息
log.debug("className ------> " + joinPoint.getTarget().getClass().getName());
log.debug("method ----> " + joinPoint.getSignature().getName());
log.debug("params ----->" + Arrays.toString(joinPoint.getArgs()));
}
//DO SOMETHING
return joinPoint.proceed();
}

/**
* 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】
* @param joinPoint
*/
@AfterReturning("myLog()")
public void afterReturning(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】");
}
}

/**
* 异常返回通知【在连接点抛出异常后执行】
* @param joinPoint
*/
@AfterThrowing("myLog()")
public void afterThrowing(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 异常返回通知【在连接点抛出异常后执行】");
}
}

/**
* 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】
* @param joinPoint
*/
@After("myLog()")
public void after(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】");
}
}
}

正常情况下,各个通知的执行顺序如下:

  • 环绕通知
  • 前置通知
  • 返回通知
  • 正常返回通知

在发生异常的情况下,执行顺序如下:

  • 环绕通知
  • 前置通知
  • 返回通知
  • 异常返回通知

一般我们只需要配置环绕通知和异常返回通知即可

然后我们添加Controller类进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class HelloController {

@RequestMapping("/")
public String hello() {
return "hello";
}

@GetMapping("/user")
@MyLog
public String user(@RequestParam String username) {
return "hello " + username;
}
}

在 HelloController 类中,我们只对 user() 方法添加了 MyLog 注解,使用Postman进行接口测试

调用 hello() 方法未打印出 MyLogAspect 中的日志信息, 而调用 user() 方法则打印出各个方法内的日志信息

1
2
3
4
5
6
7
8
9
10
11
12
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'myLogAspect' 
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - URL ------> http://127.0.0.1:8080/user
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - METHOD ----> GET
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - ContentType ----->null
151080 2018-12-15 15:47:17.331 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - Parameters ----->org.apache.catalina.util.ParameterMap@5a243ff8
151080 2018-12-15 15:47:17.331 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - className ------> com.example.demo.controller.HelloController
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - method ----> user
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - params ----->[qinghuazangshui]
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】
151084 2018-12-15 15:47:17.335 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】
151085 2018-12-15 15:47:17.336 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】

注解内容丰富

可以在 MyLog 中添加方法,为注解添加相关说明。

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {

ActionPolicy action();
}

我们在 MyLog 中添加 action() 方法, 该方法的作用是指定用户操作的类型,返回值为 ActionPolicy。 ActionPolicy 是自定义的枚举类,定义了登录、登出、增删改查、上传、下载等操作类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum ActionPolicy {

LOGIN,

LOGOUT,

SAVE,

DELETE,

UDDATE,

SEARCH,

UPLOAD,

DOWNLOAD
}

在使用 MyLog 时需要添加 action 值的声明,否则会发生编译错误

1
2
3
4
5
@GetMapping("/user")
@MyLog(action = ActionPolicy.SEARCH)
public String user(@RequestParam String username) {
return "hello " + username;
}

在 MyLogAspect 的环绕通知方法下加入 action 的判断,进行相关的业务处理

1
2
3
4
5
6
7
8
9
10
@Around("myLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
MyLog myLog = method.getAnnotation(MyLog.class);
ActionPolicy actionPolicy = myLog.action();
if (actionPolicy == ActionPolicy.SEARCH) {
log.debug("MyLog日志记录 ---------> 查询方法调用");
}
return joinPoint.proceed();
}

同理,也可以增加其他的属性方法,仿照 action() 方法即可