写点什么

一文读懂 Annotation

作者:老周聊架构
  • 2023-04-13
    广东
  • 本文字数:4776 字

    阅读完需:约 16 分钟

一、什么是注解

根据 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)@Documentedpublic @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)@Documentedpublic @interface PrintLog {}
复制代码


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


@Component@Aspect@Slf4jpublic 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 自定义注解即可;当某个方法上使用了自定义注解,那么这个方法就相当于一个切点,那么就会对这个方法做环绕(方法执行前和方法执行后)增强处理。


@RestControllerpublic 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 切面织入底层是如何实现的呢?​有了这两点疑问,所以老周才下写下这篇文章,那下来两篇就会对这个两个疑点进行揭秘,敬请期待。


发布于: 刚刚阅读数: 3
用户头像

🏆 InfoQ写作平台-签约作者 🏆 2019-03-07 加入

微信公众号:老周聊架构

评论

发布
暂无评论
一文读懂Annotation_三周年连更_老周聊架构_InfoQ写作社区