高级工程师-Java注解

高级工程师 -Java 注解

前言

代码,就是我们身为程序员的名片。

简洁,优雅,统一,是我们的追求。

优秀的代码,会给浏览者一种艺术的美感。如 DL 大神的 JUC 包,感兴趣的小伙伴,可以研究一下。

那么日常中,各位看到的优秀代码,有着哪些特点呢?充分利用的工具类(lang3,lombok,Validation 等等),完善的注解,统一的代码规范等等。还有的,就是 Java 语言的诸多高级特性(lambda,stream,io 等)。

Java 语言中,有三个特性,是高级工程师不可或缺的:

  • 注解
  • 反射
  • 泛型

如果代码中,存在这些东西,那么即使应用得还不够合理,也能够从侧面证明这位程序员的技术追求。

这三点是初级工程师很难掌握的,因为缺乏了解与需求(或者说想不到对应的需求)。而高级工程师为了给出更加具有通用性,业务无侵入的代码,就常常需要与这些特性打交道。

在不断积累后的今天,我觉得我可以尝试写一写自己对这些特性的认识了。

今天就从注解开始,阐述我对高级工程师的一些编码认识。

简介

我发现很多小伙伴总是在喜欢记忆一些注解的功能,比如表示非空的 @NotNull 等。

这里,我要从功能与原理角度说明两点:

  • 功能:注解是一种“增强型”的注释。只不过相对于只能给人看的注释,注解可以给电脑(JVM,程序等)看。
  • 原理:注解的底层是 Annotation 接口的继承者。只不过相对于日常使用的接口,注解需要使用 @interface,但是编译的结果依旧是接口继承(如 TestAnnotation extend Annotation)。

请大家牢记上面两点,这是有关注解认识的绝对核心

只要大家抓住这两个角度去认识注解,那么很快就可以成为注解达人。后续很多阐述都会从这两个角度,去为大家解释。如为什么人们常说注解是无法继承的,为什么需要元注解等等。

注解的目录结构

在这里插入图片描述

其实可以看到,JDK 中有关注解的内容很少,非常适合作为三大特性的入门啊。因为注解的实现基础是存在于 JVM 中的,JDK 只是提供了对应的工具。

Annotation 接口

上面提到注解的底层是接口,这里以图为证。

在这里插入图片描述

注意,仔细看这个接口的注释。注释中明确提出,虽然注解的本质是接口。但是直接引用 Annotation 接口,是无法实现注解功能的。

元注解

简介

通俗来说,元注解就是注解的注解。

首先元注解,是 Java 自带的预置注解。从这个角度,需要与 @XXX 修饰的自定义注解进行区分。

站在功能上来说,元注解就是专门修饰注解的“注释”,用来告诉编译器,虚拟机,相关的信息(如运行时间,目标对象)。

站在原理上来说,元注解也是注解,也是使用了 @interface(底层依旧是继承 Annotation 接口)。

不过注解,在底层实现已经继承 Annotation 接口,那么就无法通过继承接口的方式(Java 不支持多重继承),来保存元注解的信息(尤其这个信息往往不止一类)。那么注解的元注解信息是如何保存,并交给计算机的呢?答案就是通过 RuntimeVisibleAnnotations 进行相关信息的保存的。以下就是对 DynamicPropertyVerification 注解反编译的结果,重点在于反编译结果的最后一段。


	Classfile /D:/IDEA_Project/IdeaProjects/learning/demo/target/classes/tech/jarry/learning/demo/common/anno/DynamicPropertyVerification.class
	  Last modified Apr 12, 2020; size 899 bytes
	  MD5 checksum 72657e8b89f0de070bf7085b0dd975da
	  Compiled from "DynamicPropertyVerification.java"
	public interface tech.jarry.learning.demo.common.anno.DynamicPropertyVerification extends java.lang.annotation.Annotation
	  minor version: 0
	  major version: 52
	  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
	Constant pool:
	   #1 = Class              #28            // tech/jarry/learning/demo/common/anno/DynamicPropertyVerification
	   #2 = Class              #29            // java/lang/Object
	   #3 = Class              #30            // java/lang/annotation/Annotation
	   #4 = Utf8               message
	   #5 = Utf8               ()Ljava/lang/String;
	   #6 = Utf8               AnnotationDefault
	   #7 = Utf8               property verification fail
	   #8 = Utf8               groups
	   #9 = Utf8               ()[Ljava/lang/Class;
	  #10 = Utf8               Signature
	  #11 = Utf8               ()[Ljava/lang/Class<*>;
	  #12 = Utf8               payload
	  #13 = Utf8               ()[Ljava/lang/Class<+Ljavax/validation/Payload;>;
	  #14 = Utf8               SourceFile
	  #15 = Utf8               DynamicPropertyVerification.java
	  #16 = Utf8               RuntimeVisibleAnnotations
	  #17 = Utf8               Ljava/lang/annotation/Documented;
	  #18 = Utf8               Ljava/lang/annotation/Target;
	  #19 = Utf8               value
	  #20 = Utf8               Ljava/lang/annotation/ElementType;
	  #21 = Utf8               FIELD
	  #22 = Utf8               Ljava/lang/annotation/Retention;
	  #23 = Utf8               Ljava/lang/annotation/RetentionPolicy;
	  #24 = Utf8               SOURCE
	  #25 = Utf8               Ljavax/validation/Constraint;
	  #26 = Utf8               validatedBy
	  #27 = Utf8               Ltech/jarry/learning/demo/common/anno/DynamicPropertyVerificationValidator;
	  #28 = Utf8               tech/jarry/learning/demo/common/anno/DynamicPropertyVerification
	  #29 = Utf8               java/lang/Object
	  #30 = Utf8               java/lang/annotation/Annotation
	{
	  public abstract java.lang.String message();
	    descriptor: ()Ljava/lang/String;
	    flags: ACC_PUBLIC, ACC_ABSTRACT
	    AnnotationDefault:
	      default_value: s#7
	  public abstract java.lang.Class<?>[] groups();
	    descriptor: ()[Ljava/lang/Class;
	    flags: ACC_PUBLIC, ACC_ABSTRACT
	    AnnotationDefault:
	      default_value: []Signature: #11       // ()[Ljava/lang/Class<*>;
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> java.lang.Class&lt;? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">javax</span>.validation.Payload&gt;[] payload();
    descriptor: ()[Ljava/lang/Class;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: []Signature: #<span class="hljs-number">13</span>       <span class="hljs-comment">// ()[Ljava/lang/Class&lt;+Ljavax/validation/Payload;&gt;;</span>
}
SourceFile: <span class="hljs-string">"DynamicPropertyVerification.java"</span>
RuntimeVisibleAnnotations:
  <span class="hljs-number">0</span>: #<span class="hljs-number">17</span>()
  <span class="hljs-number">1</span>: #<span class="hljs-number">18</span>(#<span class="hljs-number">19</span>=[e#<span class="hljs-number">20.</span>#<span class="hljs-number">21</span>])
  <span class="hljs-number">2</span>: #<span class="hljs-number">22</span>(#<span class="hljs-number">19</span>=e#<span class="hljs-number">23.</span>#<span class="hljs-number">24</span>)
  <span class="hljs-number">3</span>: #<span class="hljs-number">25</span>(#<span class="hljs-number">26</span>=[c#<span class="hljs-number">27</span>])

最后一段,通过 RuntimeVisibleAnnotations,保存了所需要的元注解信息。

如果对 JVM 底层原理有了解的小伙伴,应该对 RuntimeVisibleAnnotations 不陌生。不了解的小伙伴,可以查看Class RuntimeVisibleAnnotations

预置注解

元注解

元注解是 Java 自带的,主要分为:

  • @Rentention:表示目标注解的保持策略。其 value 为 RetentionPolicy。如果目标注解没有使用该注解,则默认使用 RetentionPolicy.CLASS
  • @Target:表示目标注解的应用目标类型。其 value 为 ElementType。如果目标注解没有使用该注解,则目标注解可以用于除了 TYPE_PARAMETER 和 TYPE_USE 以外的任何地方(这两个类型都是 Java8 新添加的)。
  • @Documented:表示目标注解可以出现在 JavaDoc 中。
  • @Repeatable:表示目标注解可以在同一位置,重复使用。
  • @Inherited:表示目标注解可以随着所修饰的类的继承关系,被子类继承。

@Retention

源码:


	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.ANNOTATION_TYPE)
	public @interface Retention {
	    /**
	     * Returns the retention policy.
	     * @return the retention policy
	     */
	    RetentionPolicy value();
	}

通过 RetentionPolicy 枚举表示目标注解的保持策略。


	public enum RetentionPolicy {
	    /**
	     * 目标注解会在编译期丢失
	     */
	    SOURCE,
    <span class="hljs-comment">/**
     * 默认行为。虽然目标注解会通过编译,保存至.class文件中,但是JVM不会在运行时识别该注解。
     */</span>
    CLASS,

    <span class="hljs-comment">/**
     * 常用行为。目标注解会保存至.class文件中,JVM会在运行时识别,并记录该注解。所以可以通过反射获取对应的信息。
     * 详见 java.lang.reflect.AnnotatedElement
     */</span>
    RUNTIME
}

为了便于大家理解,这里再举一些例子。这里挑选一些 Java 自带的,不用大家再去自己写 demo,增加认知负荷:

  • @Retention(RetentionPolicy.SOURCE):如 @Override 注解,由于该注解只是用于进行代码检测,所以只要存在于源码中即可,故选择 RetentionPolicy.SOURCE。类似的还有 @SuppressWarnings 注解等。
  • @Retention(RetentionPolicy.CLASS):涉及注解处理器,所以实例很少。可以查看自定义注解之编译时注解 (RetentionPolicy.CLASS)(一)
  • @Retention(RetentionPolicy.RUNTIME):如 @Deprecated,由于该注解需要在运行时提示用户注解修饰的方法,类等已经过时,所以需要 JVM 中有对应“注释”信息,故采用 RetentionPolicy.RUNTIME。类似的还有 @Repeatable 等。

@Target


	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.ANNOTATION_TYPE)
	public @interface Target {
	    /**
	     * Returns an array of the kinds of elements an annotation type
	     * can be applied to.
	     * @return an array of the kinds of elements an annotation type
	     * can be applied to
	     */
	    ElementType[] value();
	}

通过 ElementType 枚举表示目标注解的应用目标类型。


	public enum ElementType {
	    /** 类,接口(包括注解,即 Annotation 接口),或者枚举类型 */
	    TYPE,
    <span class="hljs-comment">/** 属性 (包括枚举常量,枚举常量示例:Retention.SOURCE) */</span>
    FIELD,

    <span class="hljs-comment">/** 方法 */</span>
    METHOD,

    <span class="hljs-comment">/** 形参(形式参数) */</span>
    PARAMETER,

    <span class="hljs-comment">/** 构造器 */</span>
    CONSTRUCTOR,

    <span class="hljs-comment">/** 本地变量 */</span>
    LOCAL_VARIABLE,

    <span class="hljs-comment">/** 注解类型 */</span>
    ANNOTATION_TYPE,

    <span class="hljs-comment">/** 包 */</span>
    PACKAGE,

    <span class="hljs-comment">/**
     * 类型参数(针对数据类型)
     * <span class="hljs-doctag">@since</span> 1.8
     */</span>
    TYPE_PARAMETER,

    <span class="hljs-comment">/**
     * 类型(功能域包含PARAMETER与TYPE_PARAMETER)
     * <span class="hljs-doctag">@since</span> 1.8
     */</span>
    TYPE_USE
}

这里不会一一举例,只会点出重点:

  • TYPE_PARAMETER 与 TYPE_USE 是 Java8 新增加的。所以使用 Java7 的小伙伴要注意。
  • ElementType.TYPE 涵盖范围很广泛,在不知用哪个时,可以先用这个。

@Documented

默认情况下,注解是不出现在 javadoc 中的。通过给目标注解加上 @Documented 元注解,能使目标注解出现在 javadoc 中。

在这里插入图片描述

从源码可以看出,@Documented 是一个没有任何成员的标记注解。

@Repeatable

在这里插入图片描述

@Repeatable 注解的使用,引用一个不错的demo


	package com.zejian.annotationdemo;
	import java.lang.annotation.*;/**
	* Created by zejian on 2017/5/20.
	*/
	@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
	@Retention(RetentionPolicy.RUNTIME)
	@Repeatable(FilterPaths.class)
	public @interface FilterPath {
	   String  value();
	}
<span class="hljs-meta">@Target(ElementType.TYPE)</span>
<span class="hljs-meta">@Retention(RetentionPolicy.RUNTIME)</span>
<span class="hljs-meta">@interface</span> FilterPaths {
   FilterPath[] value();
}

<span class="hljs-meta">@FilterPath("/web/update")</span>
<span class="hljs-meta">@FilterPath("/web/add")</span>
<span class="hljs-meta">@FilterPath("/web/delete")</span>

上述代码,其实分为两个部分:

  • 使用 @Repeatable 注解,使得其修饰的 @FilterPath,可以在目标上重复标记(便于设置不同的成员变量)。
  • 通过 @FilterPaths 注解(包含成员变量 -FilterPath[] value ()),将 @FilterPath 集中到 @FilterPaths 中,便于后续逻辑处理。

@Inherited

@Inherited 同样是只能修饰注解的元注解,它所标注的目标注解具有继承性。

这里解释一下这个继承性,这并不是注解间的继承。而是指目标注解可以随着类的继承,而被子类继承。简单说,就是目标注解修饰的类,其后代类也会被该注解标注(可以通过 getAnnotation 方法获取)。

在这里插入图片描述

这里不再赘述,感兴趣的小伙伴,可以查看Java 注解(Annotation)中的相关示例。

功能注解

Java 预置的功能注解,主要分为:

  • @Override:该注解修饰的目标方法,必须是重写基类方法,或实现对应接口方法,否则编译器会报错。
  • @Deprecated:该注解修饰的目标,表示已经过时,不推荐使用。编码时,使用该注解的目标,会有划线提示。
  • @SuppressWarnings:该注解修饰的目标,将会忽略某些异常(由注解的 value 指定),从而通过编译器编译。
  • @SafeVarargs:该注解修饰的构造函数(只能修饰构造函数),将会忽略可变参数带来的警告。该注解于 Java7 引入。
  • @FunctionalInterface:该注解修饰的接口,为函数式接口。如 java.util.function 下的 Consumer 接口,作为一个函数式接口,被该注解修饰(函数式接口不一定有该注解修饰,但被该注解修饰的接口,一定是函数式接口)。

自定义注解

到了这里,大家应该对注解不再陌生了。
而在日常开发中,我们常常需要自定义开发一些注解。
自定义注解分为以下步骤:

  1. [必选] 使用 @interface 来构建自定义注解。一般在创建自定义注解的同时,就达成了该要求。
  2. [可选] 使用 @Target 元注解。通过该注解,确定自定义注解的作用目标类型。注意:如果目标注解没有使用该注解,则目标注解可以用于除了 TYPE_PARAMETER 和 TYPE_USE 以外的任何地方(这两个类型都是 Java8 新添加的)。
  3. [可选] 使用 @Retention 元注解。通过该注解,明确自定义注解的生命周期,或者说自定义注解作用域。如果目标注解没有使用该注解,则默认使用 RetentionPolicy.CLASS
  4. [可选] 添加成员变量。格式为“long value()default 1000L;”,与 Java8 的接口成员变量非常类似。注意:注解的成员变量只能采用无参方法表示。并且注解的成员变量,只能采用基本数据类型 (char,boolean,byte、short、int、long、float、double) 和 String、Enum、Class、annotations 数据类型, 以及这一些类型的数组。
  5. [可选] 使用自定义注解。自定义注解的使用领域很多,主要分为两个方向:
    • 利用已有框架,不需要自己实现相关逻辑,自定义注解多作为标记注解。如配合 SpringBoot 的注解,形成自己的注解(相关的逻辑由 SpringBoot 自己处理)
    • 利用已有框架,需要自己实现部分逻辑(不涉及反射),但需要关联已有框架,并实现对应接口。如 Validation 框架的自定义校验注解,感兴趣的小伙伴,可以查看我之前写的Validation 框架的应用
    • 可选择已有框架,需要自己实现诸多逻辑。如在 AOP 中,我们常常需要通过反射,获取自定义注解的信息(如参数等),或者自定义注解修饰的目标的信息(如参数,方法名等)。这部分,我会在后续的反射部分详细说明。

总结

简单总结一下,本文主要描述了:

  1. 注解是什么:增强型的注释,本质是接口
  2. 元注解是什么:注解的注解,作用是为了标识目标注解。包括 @Target,@Retention,@Documented,@Repeatable,@Inherited.
  3. 预置注解是什么:JDK 自带的经典功能注解,如 @Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface。
  4. 自定义注解如何实现:主要分为五步,但是其中必要的步骤,就一步:使用 @interface 来构建自定义注解。

至此,Java 注解的内容就基本展现了。

最后,还是强调两个方面:

  1. 注解就是增强型的注释(可被计算机识别的注释),本质是接口。把握住这两点,就非常好理解注解与它的各种规则,行为。
  2. 注解本身并没有任何功能(因为它只是注释,本质也只是接口),需要其他代码支撑,它才能体现价值。

希望对大家有所帮助,还有不清楚的地方,可以查看下列参考目录。

愿与诸君共进步。

附录

参考