元注解
在 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() {
}
@Before("myLog()") public void before(JoinPoint joinPoint) { if (log.isDebugEnabled()) { log.debug("MyLog日志注解 - 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】"); } }
@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())); } return joinPoint.proceed(); }
@AfterReturning("myLog()") public void afterReturning(JoinPoint joinPoint) { if (log.isDebugEnabled()) { log.debug("MyLog日志注解 - 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】"); } }
@AfterThrowing("myLog()") public void afterThrowing(JoinPoint joinPoint) { if (log.isDebugEnabled()) { log.debug("MyLog日志注解 - 异常返回通知【在连接点抛出异常后执行】"); } }
@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: 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() 方法即可