一文读懂Annotation

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 一文读懂Annotation

一、什么是注解

根据wikipedia中介绍:

In the Java computer programming language, an annotation is a form of syntactic metadata that can be added to Java source code. Classes, methods, variables, parameters and Java packages may be annotated. Like Javadoc tags, Java annotations can be read from source files. Unlike Javadoc tags, Java annotations can also be embedded in and read from Java class files generated by the Java compiler. This allows annotations to be retained by the Java virtual machine at run-time and read via reflection. It is possible to create meta-annotations out of the existing ones in Java.

翻译中文则是:

Java 注解又称 Java 标注,是 JDK5.0 版本开始支持加入源代码的特殊语法元数据 。

Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。当然它也支持自定义 Java 标注。

这定义已经够清晰了,但你可能有个疑问,老周啊,通过反射获取标注内容,反射在哪体现啊。别问,一问又是要看底层源码了。

既然这样,那我们就来看下 JDK java.lang.annotation 包的结构:

看见带 @ 标识的没,一共有 6 个,所以 JDK 源码里定义了 6 个注解。

  • @Document
  • @Target
  • @Retention
  • @Inherited
  • @Native
  • @Repeatable

其中前 4 个是元注解。

等一等,元注解又是什么?不急,我们来看下一节。

二、元注解

元注解的作用就是负责注解其它注解,它们被用来提供对其它 annotation 类型作说明。

我们拿 @Document 元注解来说吧。

@Documented@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)public 
@interface Documented {
}

不管是这里的 Documented 还是我们自定义的注解,都需要使用 @interface 标识,使用了这个标识,会自动继承 java.lang.annotation.Annotation 接口,由编译程序自动完成其它细节。你又可能会说了,为啥是这样的标识就会自动继承这个接口呀。额,这是人家的事先约定的规范,如果你是 JDK 源码的开发人员,也可以自己定义其它标识,亦或是写了一套比注解还好用的规范呢。

在定义注解时,不能继承其它的注解或接口。@interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default 来声明参数的默认值。

1、Annotation 类型里面的参数该如何设定

  • 只能用 public 或默认(default)这两个修饰访问权限。例如 String value(); 这里把方法设为 defaul 默认类型。
  • 参数成员只能用【char、byte、short、int、long、float、double、boolean】八种基本数据类型和 String、Enum、Class 和 annotations 等数据类型,以及这一些类型的数组。例如 String value(); 这里的参数成员就为 String。
  • 如果只有一个参数成员,最好把参数名称设为 “value”,后加小括号。

2、元注解的用途

在详细说这四个元数据的含义之前,先来看一个在工作中会经常使用到的 @Autowired 注解,此注解中使用到了 @Target、@Retention、@Documented 这三个元注解 。

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

2.1 @Target 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}


@Target 注解,是专门用来限定某个自定义注解能够被应用在哪些 Java 元素上面的,标明作用范围;取值在 java.lang.annotation.ElementType 进行定义的。

public enum ElementType {
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,
    /** 属性的声明 */
    FIELD,
    /** 方法的声明 */
    METHOD,
    /** 方法形式参数声明 */
    PARAMETER,
    /** 构造方法的声明 */
    CONSTRUCTOR,
    /** 局部变量声明 */
    LOCAL_VARIABLE,
    /** 注解类型声明 */
    ANNOTATION_TYPE,
    /** 包的声明 */
    PACKAGE,
    /** 作用于类型参数(泛型参数)声明 */
    TYPE_PARAMETER,
    /** 作用于使用类型的任意语句(不包括class) */
    TYPE_USE
}

根据此处可以知道 @Autowired 注解的作用范围:

// 可以作用在 构造方法、方法、方法形参、属性、注解类型 上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})

2.2 @Retention 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

@Retention 注解,翻译为持久力、保持力。即用来修饰自定义注解的生命周期。

注解的生命周期有三个阶段:

  • Java 源文件阶段
  • 编译到 class 文件阶段
  • 运行期阶段

同样使用了 RetentionPolicy 枚举类型对这三个阶段进行了定义:

public enum RetentionPolicy {
    /**
     * 注解将被编译器忽略掉
     */
    SOURCE,
    /**
     * 注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为
     */
    CLASS,
    /**
     * 注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到
     */
    RUNTIME
}

再详细描述下这三个阶段:

  • 如果被定义为 RetentionPolicy.SOURCE,则它将被限定在 Java 源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读 Java 文件的人看到;
  • 如果被定义为 RetentionPolicy.CLASS,则它将被编译到 Class 文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时 JVM(Java虚拟机)会忽略它,并且在运行期也不能读取到;
  • 如果被定义为 RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到 Class 对象中。那么在程序运行阶段,可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。

注意:实际开发中的自定义注解几乎都是使用的 RetentionPolicy.RUNTIME 。

2.3 @Documented 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Documented 注解,是被用来指定自定义注解是否能随着被定义的 java 文件生成到 JavaDoc 文档当中。

2.4 @Inherited 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Inherited 注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。

@Inherited 注解只对那些 @Target 被定义为 ElementType.TYPE 的自定义注解起作用。

三、如何自定义注解

上面把注解与元注解说完了,那得实战一下吧。其实很多人在工作中已经用到过了或者自己没用到过但项目中有用到过。但你有没有想过自定义注解是怎么获取到值的,是的没错,就是我们开头讲的定义,通过反射。

这里我就拿一个我们项目中自定义注解的例子来说:

1、标记日志打印的自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrintLog {
}

2、定义一个切面,在切面中对使用了 @PrintLog 自定义注解的方法进行环绕增强通知

@Component
@Aspect
@Slf4j
public class PrintLogAspect {
    @Around(value = "@annotation(com.riemann.core.annotation.PrintLog)")
    public Object handlerPrintLog(ProceedingJoinPoint joinPoint) throws Throwable {
        String clazzName = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Map<String, Object> nameAndArgs = getFieldsName(this.getClass(), clazzName, methodName, args);
        log.info("Enter class[{}] method[{}] params[{}]", clazzName, methodName, nameAndArgs);
        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("Process class[{}] method[{}] error", clazzName, methodName, throwable);
        }
        log.info("End class[{}] method[{}]", clazzName, methodName);
        return object;
    }
    private Map<String, Object> getFieldsName(Class clazz, String clazzName, String methodName, Object[] args) throws NotFoundException {
        Map<String, Object > map = new HashMap<>();
        ClassPool pool = ClassPool.getDefault();
        ClassClassPath classPath = new ClassClassPath(clazz);
        pool.insertClassPath(classPath);
        CtClass cc = pool.get(clazzName);
        CtMethod cm = cc.getDeclaredMethod(methodName);
        MethodInfo methodInfo = cm.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        if (attr == null) {
            // exception
        }
        int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
        for (int i = 0; i < cm.getParameterTypes().length; i++) {
            map.put( attr.variableName(i + pos), args[i]);
        }
        return map;
    }
}

3、最后,在 Controller 中的方法上使用 @PrintLog 自定义注解即可;当某个方法上使用了自定义注解,那么这个方法就相当于一个切点,那么就会对这个方法做环绕(方法执行前和方法执行后)增强处理。

@RestController
public class Controller {
  @PrintLog
  @GetMapping(value = "/user/findUserNameById/{id}", produces = "application/json;charset=utf-8")
  public String findUserNameById(@PathVariable("id") int id) {
      // 模拟根据id查询用户名
      String userName = "公众号【老周聊架构】";
      return userName;
  }
}

4、在浏览器中输入网址:http://127.0.0.1:8080/api/user/findUserNameById/666 回车后触发方法执行,发现控制台打印了日志

Enter class[Controller] method[findUserNameById] params[{id=666}]
End class[Controller] method[findUserNameById]


这样的话,项目中的 Controller 类的请求日志我们不必每个方法都打印一遍了,而且收集日志到日志中心请求的参数也有具体统一的格式,排查问题也方便了不少。使用自定义注解 + AOP 实现日志的打印,有木有如丝滑般顺畅的感觉,哈哈,这样代码看着也优雅了不少。

这里要说一下,写这篇文章的初衷是我一个好哥们在群里提了这么个问题,找不到自定义注解和相关方法的关联。我第一时间想到的是反射,然后再想想自己项目中的场景,虽说是 AOP 实现,但注解是如何通过反射获取值的呢?AOP 切面织入底层是如何实现的呢?有了这两点疑问,所以老周才下写下这篇文章,那接下来两篇就会对这个两个疑点进行揭秘,敬请期待。



欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。


喜欢的话,点赞、再看、分享三连。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
|
Java 编译器 开发者
Java注解(Annotation)技术深入解析
Java注解(Annotation)技术深入解析
459 1
|
缓存 Java 索引
一文读懂注解的底层原理
一文读懂注解的底层原理
191 0
|
安全 Java 编译器
一文带你全面深入理解Java注解Annotation
一文带你全面深入理解Java注解Annotation
289 1
一文带你全面深入理解Java注解Annotation
|
Java 开发者
annotation 实现 | 学习笔记
快速学习 annotation 实现,介绍了 annotation 实现系统机制, 以及在实际应用过程中如何使用。
annotation 实现 | 学习笔记
|
XML 安全 Java
Java核心技术之 深入理解注解(Annotation)
Java核心技术之 深入理解注解(Annotation),深入注解原理、注解场景以及使用,重新了解注解
Java核心技术之 深入理解注解(Annotation)
|
Java 编译器 测试技术
java注解annotation学习
java注解annotation学习
125 0
|
JavaScript 前端开发
一文搞懂instanceof实现的原理是什么!
前言 类型的判断可以说在我们前端开发过程中无处不在,特别是在 Typescript 还未推出之前,我们在 JS 里面做类型判断显得就更加重要了。 判断数据类型的方式有特别多,比如大家常用的 typeof、instanceof、Object.prototype.tostring.call()等等,那么每一种判断数据类的方法大家知道其中的原理吗?比如说 instanceof 的原理,今天我们就聊一聊 instanceof 是如何判断数据类型的。
815 0
一文搞懂instanceof实现的原理是什么!
|
Java 编译器 Spring
Java注解(Annotation)的基本原理以及实现自定义注解
在我们使用springboot的时候我们知道因为注解的存在,使得我们的开发变得格外的方便、快捷。之前的文章Spring常用注解大全,值得你的收藏!!!对于spring中各类注解也进行过介绍。然而注解也并不是因为spring框架的兴起才出现的,而是很早就已经在java中被使用。
1267 0
Java注解(Annotation)的基本原理以及实现自定义注解
|
开发框架 Java 编译器
Java注解Annotation小结
Java注解Annotation小结
|
前端开发 安全 Java
Java注解(Annotation):请不要小看我!(1)
Java注解(Annotation):请不要小看我!
121 0
Java注解(Annotation):请不要小看我!(1)