“
人生苦短,不如养狗
”
一、什么是注解
自JDK 5以后,JDK就提供了一种用于描述类、变量、方法和方法参数等的信息的类—— 注解类 。简单理解,可以认为注解类是一种 能够被程序识别处理 的 程序级别 的“注释”。
通过注解类,开发人员可以对类、变量、方法或方法参数进行一些特殊的标记和描述,然后在编译期或运行期可以针对这些被特殊标记或描述的类、变量、方法或方法参数进行一些特殊的操作。
从JDK的源码当中可以了解到,所有的注解都是接口 Annotation
的实现类,但是这个实现的声明并显示地在代码层面进行展示,而需要通过编译后的二进制文件进行观察。这里我们可以看下元注解 Retention
的二进制文件:
除此以外,还需要注意注解类实际上是一种特殊的接口声明,但是为了和普通的接口声明进行区分,在关键字 interface
前面添加了一个 @ 进行特殊声明。这也从另一个方面说明了注解本身是没有任何处理逻辑的,只是标注了一些元数据信息,至于如何去处理这些元数据信息,则需要通过其他手段进行处理。
二、Java中的基础注解
在Java中提供了两类基础注解以供开发使用和进行自定义注解的扩展,分别是如下两种:
-
元注解
: 用于标记和描述注解最基本信息的注解,是JDK中最基础的注解。对于每个注解都需要指明其 保留的时间 和 生效的上下文 这些最基本的信息,JDK原生的元注解则提供了可以用于标注并描述这些信息的注解。具体元注解有以下几种: -
RetentionPolicy.Source
: 只存在于源代码中,编译期就会被丢弃。 -
RetentionPolicy.CLASS
: 注解会在编译期被写入class文件中,但在运行期会被VM丢弃,是默认的保留策略。 -
RetentionPolicy.RUNTIME
: 注解会一直保存至运行期,即该注解可以通过反射机制获取对应的信息。 -
Retention
: 指明带有该注解的注解将保留多长时间,是最重要的两个元注解之一。所有的注解都需要设置@Retentioin
来表明其存活的生命周期,如果不做设置则其保留策略默认为RetentionPolicy.CLASS
。需要注意的是,一个注解只能设置一个保留策略。具体策略如下: -
Target
: 指明带有该注解的注解所适用的上下文,是最重要的两个元注解之一。对于@Target
的取值可以参考枚举类ElementType
,该枚举类提供了注解出现在Java程序的语法位置的简单分类。如果没有设置的情况下,注解可以放置在除了类型以外的其他所有语法位置。 -
Documented
: 官方文档中的解释为:在默认情况下,带有类型的注解将由 javadoc 和类似工具记录。实际上就是在生成javadoc文件时会,带有该注解的注解会出现的文档中作为注释的一部分出现。 -
Inherited
: 被@Inherited
注解的注解标记一个类时,该类的子类会自动继承该注解(不需要显示声明)。 -
内置注解
: JDK提供的原生的通用注解。以下这些内置注解主要是为了方便开发进行通用场景的处理而提供的,可以理解为是JDK提供的针对某些通用场景设置的标准协议。这里列出最常见的三种内置注解: -
Override
: 该注解作用于方法级别,指明了被该注解标注的方法用于覆盖其超类当中声明的相同的方法。 -
Deprecated
: 指明了被该注解标识的方法、变量等程序元素是即将废弃,不鼓励继续使用的。一般用作版本升级中对不再适用的接口进行标注,提醒使用者使用更好的替代方案。 -
SuppressWarnings
: 官方文档中的解释为:指示应在带注解的元素(以及带注解的元素中包含的所有程序元素)中抑制指定的编译器警告。简单理解就是,如果你不想看到对应的编译器告警,可以通过这个注解进行告警屏蔽处理。
三、如何处理一个注解
注解与注释最大的区别在于,注解可以通过程序进行读取和操作,注释则不行。根据注解的保留策略,JDK提供了两种方式进行注解的处理:插入式注解处理器 和 反射机制 。下面我们就来具体看下这两种处理方式。
1. 插入式注解处理器
JDK在1.6版本之后提供了一种可以在编译期进行注解读取和处理的能力,即 插入式注解处理器 ,开发可以通过实现JDK的API自定义注解处理器实现干涉编译器的行为。这里不得不提到业界大名鼎鼎的提效神器Lombok,Lombok就是通过这种方法来实现通过注解的方式在编译期生成诸如setter、getter等方法的。
首先来看一下Java编译过程的简图:
解析和填充符号表过程实际上是将源码分析转换成一个 抽象语法树(AST) 。这里引入百度百科对于抽象语法树的解释:
“
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
”
而在插入式注解处理器进行注解处理的过程会根据注解对这棵抽象语法树进行读取、修改和填充处理,如果在这一过程中发生了修改语法树的情况,编译器会重新进入解析和填充符号过程,一直循环直到抽象语法树不再发生任何变更。
由于语法树当中的任意元素可以被读取、修改,开发人员就能够将很多需要人工编码的工作通过自定义注解处理器的方式在编译期自动完成这些工作,比如上面的Lombok自动生成setter/getter方法等。
2. 反射机制
除了在编译期通过插入式注解处理器对注解进行处理,我们还可以使用反射机制对注解进行处理,当然这里需要将注解的保留策略设置为 RetentionPolicy.RUNTIME
。
运行期间使用反射机制来进行注解的读取、修改和编译期使用插入式注解处理器处理相比会有性能上的损耗,但是结合AOP还是能够很好的实现类似日志打印切面等非程序运行时的业务逻辑处理工作。
四、编写一个自定义注解并处理
这里我们编写一个自定义注解来尝试用插入式注解处理器来处理这个自定义注解。为了使用插入式注解处理器,我们需要创建 两个maven项目 ,注意是两个项目,其中一个为自定义注解处理器项目,一个为测试使用项目。
1. 自定义注解处理器项目
注解 IntegerCheck
package annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 整型类型检查
*
* @author brucebat
* @version 1.0
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface IntegerTypeCheck {
/**
* 名称
*
* @return 注解名称
*/
String name() default "整型类型检查器";
}
自定义注解处理器 IntegerTypeCheckProcessor
package annotation.processor;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import java.util.List;
import java.util.Set;
/**
* 整型类型检查
*
* @author brucebat
* @version 1.0
*/
@SupportedAnnotationTypes("annotation.IntegerTypeCheck")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class IntegerTypeCheckProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getRootElements();
if (null == elements || elements.isEmpty()) {
return false;
}
for (Element element : elements) {
System.out.println("当前元素名称为 : " + element.getSimpleName().toString());
TypeElement typeElement = (TypeElement) element;
List<? extends Element> enclosedElements = typeElement.getEnclosedElements();
if (null == enclosedElements || enclosedElements.isEmpty()) {
System.out.println("变量不存在");
continue;
}
for (Element enclosedElement : enclosedElements) {
System.out.println("当前变量名称为 : " + enclosedElement.getSimpleName());
if (enclosedElement instanceof VariableElement) {
System.out.println("当前变量元素是field元素, 名称为 : " + enclosedElement.getSimpleName());
}
}
}
return false;
}
}
pom文件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
<!-- 这里添加这行是为了不进行注解处理器执行,这里只需要进行打包即可 -->
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
除了上面的代码,在这个项目中还需要在 META-INFO/services
路径下创建文件 javax.annotation.processing.Processor
,在该文件中写入自定义注解的全限定名。最后执行 mvn clean install
进行打包操作。
2. 测试项目
测试类 User
package test;
import annotation.IntegerTypeCheck;
/**
* @author brucebat
* @version 1.0
*/
public class User {
@IntegerTypeCheck
private String name;
@IntegerTypeCheck
private Integer age;
}
pom文件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
<annotationProcessors>
<annotationProcessor>
annotation.processor.IntegerTypeCheckProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
由于在这个项目中需要使用对应的自定义注解处理器,所以需要在编译插件中加入对应的注解处理器,然后执行 mvn compile
,最终执行结果如下:
当前元素名称为 : User
当前变量名称为 : <init>
当前变量名称为 : name
当前变量元素是field元素, 名称为 : name
当前变量名称为 : age
当前变量元素是field元素, 名称为 : age
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
五、总结
其实从上面的例子可以看到,我们处理的并不是注解,而是注解标识的类、变量、方法等程序中的元素。对于上面列出的两种处理方式,插入式注解处理器更偏向于程序的预处理,比如进行代码生成、命名规范检查等,而对于反射机制则能够在程序运行期间对对象实例进行参数校验、日志打印等处理,两者各有其适用场景。
本文更多是通过例子让大家感受一下插入式注解处理器是如何编写和使用,如果想要进一步了解对应的API和如何使用注解处理器处理AST,可以阅读一下源码研究一下。
原文始发于微信公众号(Brucebat的伪技术鱼塘):探秘Java:那些你熟悉又陌生的注解
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/90103.html