在介绍注解之前,提出几个问题,后面的内容也会围绕着这几个问题解答。
1、什么是注解?注解是类or接口?注解主要分为哪几类?
2、注解可以干什么?注解是如何生效的?
3、什么是注解处理器?如何实现自定义注解?
一、什么是注解?
1、Java1.5中对注解的定义
注解在Java的1.5版本引入,其定义为提供一种元数据中的元素关联任何信息和任何元数据的途径和方法,简而言之就是为类、方法、字段等Java结构提供额外信息的一种机制。
2、注解是类or接口?
在编辑器中,可以创建类、接口、枚举和注解等类型的java文件,创建如下自定义注解
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {`
}
采用javac命令编译,采用java -verbose命令获取到对应的可读的字节码文件如下:
Classfile /D:/project/project/annosource/src/main/java/com/zzr/annosource/PrintMe.class //字节码文件全路径
Last modified 2020-10-28; size 293 bytes
MD5 checksum 6e0b53143b1b67e21cffd76974a7e67a
Compiled from “PrintMe.java”
public interface com.zzr.annosource.PrintMe extends java.lang.annotation.Annotation //可以看出注解是一种接口
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
#1 = Class #11 // com/zzr/annosource/PrintMe
#2 = Class #12 // java/lang/Object
#3 = Class #13 // java/lang/annotation/Annotation
#4 = Utf8 SourceFile
#5 = Utf8 PrintMe.java
#6 = Utf8 RuntimeVisibleAnnotations
#7 = Utf8 Ljava/lang/annotation/Retention;
#8 = Utf8 value
#9 = Utf8 Ljava/lang/annotation/RetentionPolicy;
#10 = Utf8 SOURCE
#11 = Utf8 com/zzr/annosource/PrintMe
#12 = Utf8 java/lang/Object
#13 = Utf8 java/lang/annotation/Annotation
{
}
SourceFile: “PrintMe.java”
RuntimeVisibleAnnotations:
0: #7(#8=e#9.#10)
可以很明显的看出,注解是一种接口,一种继承Annotation接口的接口(在Java8中已经支持接口继承)
public interface Annotation {
boolean equals(Object var1);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
3、JDK现有注解
根据注解的作用和来源,注解主要包括JDK自带注解、元注解、自定义注解等。
在JDK中提供的注解,主要包括如下三个:@Deprecated,@SuppressWarnings,@Override
① @Deprecate—告知编译器,该方法已被废弃,建议调用方不要使用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Deprecated {
}
② @SuppressWarnings—忽略相关警告信息
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Deprecated {
}
③ @Override—告知编译器该方法是重写了父类方法
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
对于上述三个JDK注解,@Override和@SuppressWarnings都是在编译期间生效,@Deprecate则是在运行期间以及运行前的阶段生效。
4、常用元注解
元注解:注解注解的注解(禁止套娃),常用的元注解主要包括@Target、@Retention、@Documented、@Inherited等,@Target、@Retention为常用元注解
①元注解——@Target:描述注解的作用域
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
包含一个变量,ElementType的数组,为注解的作用域,枚举ElementType主要包括如下类型,其中常用的为TYPE、FIELD、METHOD。
public enum ElementType {
TYPE, //类、接口、枚举
FIELD, //成员变量
METHOD, //方法
PARAMETER, //方法参数
CONSTRUCTOR, //构造方法
LOCAL_VARIABLE, //局部变量
ANNOTATION_TYPE, //注解
PACKAGE, //包
TYPE_PARAMETER, //类型参数
TYPE_USE; //类型使用
private ElementType() {
}
}
②元注解——@Retention:描述注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
RetentionPolicy value();
}
包含一个变量RetentionPolicy ,RetentionPolicy 主要包括:
public enum RetentionPolicy {
SOURCE, //源码阶段,只生效于.java文件,常用于检测文件的合规性
CLASS, //字节码阶段,生效于.java文件和.class文件,运行时失效,常用于生成新的字节码文件
RUNTIME; //运行阶段,生效于.java文件和.class文件,可以在运行时通过反射的方式,获取注解类实现相关功能
private RetentionPolicy() {
}
}
可以看出元注解@Target和元注解@Retention的生命周期都是从源代码到运行时。
③元注解——@Documented:生成javadoc文件时,也就是API帮助文档时,保留该注解的信息
④元注解——@Inherited:父类的注解会被子类继承,只对类生效,接口实现不继承,不常用
从上面的源码可以看出元注解的作用域均为注解。
二、注解的作用
1、注解可以干什么?
① 现有的Springboot、Springcloud框架、第三方开源库均采用了大量的注解,熟悉注解能更好的阅读源码。
② 采用注解代码更加简洁,代码耦合度低,可复用强,代码逻辑更加清晰。
③ 掌握一种专业技能,提高自身价值。
2、注解是如何生效的?
a、想要弄清楚注解,首先要明白注解的结构。以下图的自定义注解为例
@Target({ElementType.METHOD,ElementType.FIELD}) //注解的作用域
@Retention(RetentionPolicy.SOURCE) //注解的生命周期
@Documented
@Inherited
public @interface TestAnnotation {
String value(); //注解的成员变量
int age();
String name();
}
主要由元注解修饰(主要是注解的作用域和注解的生命周求)和注解的成员变量组成,其中,注解的成员变量可以是基本数据类型、String、Class、注解、枚举。
b、创建好相应的注解后,需要采用注解处理器解析相应的注解,下面简单介绍下注解处理器,在自定义注解部分会详细介绍。
① Processor接口:注解处理器的主接口,主要包括init、process、getSupportedAnnotationTypes、getSupportedSourceVersion四个方法。
public interface Processor {
Set getSupportedOptions();
Set getSupportedAnnotationTypes();
SourceVersion getSupportedSourceVersion();
void init(ProcessingEnvironment var1);
boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);
Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}
其中,
init方法:注解处理器的初始化方法
getSupportedAnnotationTypes方法:返回注解处理器支持的类型
getSupportedSourceVersion方法:返回注解处理器支持的java版本
process方法:注解处理器的核心方法
② AbstractProcessor:实现了Processor接口,作为所有注解器的父类
public abstract class AbstractProcessor implements Processor {
protected ProcessingEnvironment processingEnv;
private boolean initialized = false;
…
}
AbstractProcessor实现了init、getSupportedAnnotationTypes和getSupportedSourceVersion方法,它的子类可以通过@SupportedAnnotationTypes和@SupportedSourceVersion注解来声明注解处理器所支持的注解类型以及Java版本,通过Processor方法处理相应的注解。
三、自定义注解
按照注解的生命周期,可以将注解分为三类,源码注解、字节码注解以及运行时注解。对于源码注解和字节码注解,会采用类似于XML文件解析的方式对源码进行解析,源码注解主要用于文件格式判断,字节码注解主要用于生成新的字节码文件,对于运行时注解,则是通过反射的方式进行注解解析。
1、源码阶段的注解——检测属性是否有get方法,对于无get方法的属性在编译时提示
a、新建注解CheckGetMethod
@Target({ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface CheckGetMethod {
}
b、新建注解处理器CheckGetterProcessor,继承AbstractProcessor,采用@SupportedAnnotationTypes注解和@SupportedSourceVersion
@AutoService(Process.class)
@SupportedAnnotationTypes({“com.zzr.annocheckget.CheckGetMethod”})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CheckGetMethodProcessor extends AbstractProcessor{
private Types typeUtils;
private Filer filer;
private Messager messager;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
typeUtils = processingEnv.getTypeUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
在初始化阶段,可以通过ProcessingEnvironment入参获取到一些实用类和对应注解类的参数。
方法 | 返回值参数类型 | 功能 |
---|---|---|
getTypeUtils() | Types | 用于类型操作 |
getFiler() | Filer | 用于创建类、文件 |
getMessager() | Messager | 用于报告错误消息、警告、日志信息 |
getElementUtils() | Elements | 用于元素操作,源文件解析 |
对于上述返回值参数类进行具体分析。
① Element类
Element对象是操作元素最主要的类,也可以通过process方法的入参RoundEnvironment获取,主要分为如下几个类型:
ExecutableElement、PackageElement、TypeElement、TypeParameterElement、VariableElement等。
ExecutableElement:类和接口下的构造方法、注解元素。
PackageElement:包和包下的成员元素。
TypeElement:类和接口下的成员以及类型的元素
TypeParameterElement:类、接口、方法等下的参数元素
VariableElement:方法参数、局部变量等元素
②Types类
主要是用于类型操作,包括java编程语言中的类型以及变量属性的类型等。在源码解析时,不能获取到相应的字节码文件信息,但是我们可以通过捕获异常的方式获取
③Filer类
主要是用于创建文件,包括createClassFile()、createSourceFile()、createResource(),分别用于类文件、源文件以及辅助资源文件的创建。
④Messager类
用于报告错误消息、警告等,调用printMessage()方法。
printMessage()方法入参 | 报告信息类型 |
---|---|
Diagnostic.Kind.NOTE | 日志信息、不终止编译过程 |
Diagnostic.Kind.WARNING | 警告信息、不终止编译过程 |
Diagnostic.Kind.ERROR | 错误信息、终止编译过程 |
⑤RoundEnvironment类
可以调用getElementsAnnotatedWith()方法获取当前注解元素的集合。
c、源码阶段的注解需要通过Element类解析源文件,针对process方法进行处理。
①通过roundEnv.getElementsAnnotatedWith()获取到所有被@CheckGetMethod注解的元素
②遍历所有元素,对于属性注解直接调用contaisGetterMethod方法判断是否含有get方法。
③对于类属性注解,通过ElementFilter.fieldsIn(element.getEnclosedElements())获取到类下的所有属性元素,遍历属性元素调用contaisGetterMethod方法进行判断。
④contaisGetterMethod方法首先是拼接get方法名,遍历当前类下的所有方法,如果有同名方法且入参为空则认定存在get方法,否则不存在get方法。
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//annotations包含了注解处理器能处理的所有注解类型
//roundEnv.getElementsAnnotatedWith()可以获取到被当前注解类注释的所有元素
//获取所有被CheckGetMethod注解的元素Element;
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(CheckGetMethod.class);
for (Element element : set){
if (element.getKind() == ElementKind.FIELD){
//处理属性上的注解
if (!contaisGetterMethod(element, element.getSimpleName().toString())){
messager.printMessage(Diagnostic.Kind.WARNING,String.format(“The field ‘%s’ has not getter method”, element.getSimpleName()));
}
}else if (element.getKind() == ElementKind.CLASS){
//处理类上的注解
for (VariableElement variableElement : ElementFilter.fieldsIn(element.getEnclosedElements())){//获取类的所有属性元素
if (!contaisGetterMethod(variableElement, variableElement.getSimpleName().toString())){
messager.printMessage(Diagnostic.Kind.WARNING,String.format(“The ‘%s’.’%s’ has not getter method”, element.getSimpleName(), variableElement.getSimpleName()));
}
}
}else {
//对于其他类型上的注解报错
messager.printMessage(Diagnostic.Kind.ERROR,String.format(“The CheckGetMethod annotation not support ‘%s’”, element.getSimpleName()));
}
}
return false;
}
//判断当前属性是否带有get+属性名的方法
private boolean contaisGetterMethod(Element element, String s) {
//获取element对应的类元素
Element classElement = element.getEnclosingElement();
//拼接getter方法名
String getter = “get” + s.substring(0,1).toUpperCase() + s.substring(1);
//遍历当前类的所有方法元素
for (ExecutableElement e : ElementFilter.methodsIn(classElement.getEnclosedElements())){
//存在同名方法 && 方法参数为空
if (e.getSimpleName().toString().equals(getter) && e.getParameters().isEmpty()){
return true;
}
}
return false;
}
d、将创建好的注解和注解处理器进行打包并上传到maven仓库,然后创建测试类
@CheckGetMethod
public class MainTest {
@CheckGetMethod
private int a;
@CheckGetMethod
private int b;
private static void printA(int a){
System.out.println(a);
}
}
e、编译测试类,可以看到如下结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNMQO7X5-1639364489599)(file:///C:/Users/ZHOUZH~1/AppData/Local/Temp/msohtmlclip1/02/clip_image001.png)]
2、字节码阶段的注解——针对项目中的VO生成相应的get和set方法
a、新建注解AddGetter
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface AddGetter {
}
b、新建注解处理器AddGetterProcessor并实现Processor方法如下
①通过roundEnv.getElementsAnnotatedWith(AddGetter.class)获取到所有被AddGetter修饰的类
②获取所有的属性字段,通过JavaWriter进行写入
③对生成的java文件进行编译生成对应的class格式的字节码文件
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AddGetter.class);
for (Element element : elements){
if (element.getKind() == ElementKind.CLASS && element instanceof TypeElement){
TypeElement typeElement = (TypeElement) element;
List variableElements = ElementFilter.fieldsIn(typeElement.getEnclosedElements());
JavaFileObject sourceFile = filer.createSourceFile(typeElement.getSimpleName());
JavaWriter javaWriter = new JavaWriter(sourceFile.openWriter());
javaWriter.emitPackage(((PackageElement)typeElement.getEnclosingElement()).getQualifiedName().toString());
javaWriter.beginType(typeElement.getSimpleName().toString(),“class”, EnumSet.of(Modifier.PUBLIC));
for (VariableElement variableElement : variableElements){
String type = variableElement.asType().toString();
String name = variableElement.getSimpleName().toString();
javaWriter.emitField(type,name,EnumSet.of(Modifier.PRIVATE));
}
for (VariableElement variableElement : variableElements){
String type = variableElement.asType().toString();
String name = variableElement.getSimpleName().toString();
javaWriter.beginMethod(type,“get”+ toUpper(name),EnumSet.of(Modifier.PUBLIC))
.emitStatement("return " + name)
.endMethod();
javaWriter.beginMethod(“void”,“set”+ toUpper(name),EnumSet.of(Modifier.PUBLIC),type,name)
.emitStatement(“this.” + name + " = "+name)
.endMethod();
}
javaWriter.endType().close();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
Iterable units = fileManager.getJavaFileObjects(sourceFile.toUri().getPath());
JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, null, null, null, units);
compilationTask.call();
fileManager.close();
}else {
messager.printMessage(Diagnostic.Kind.ERROR,“AddGetter only support class”);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
private String toUpper(String name) {
return name.substring(0,1).toUpperCase()+name.substring(1);
}
c、将创建好的注解和注解处理器进行打包并上传到maven仓库,然后创建测试类
@AddGetter
public class TestClass {
private int testFeild;
private String b;
public boolean c;
}
d、编译测试类,可以生成如下的class文件
public class TestClass {
private int testFeild;
private String b;
private boolean c;
public int getTestFeild() {
return testFeild;
}
public void setTestFeild(int testFeild) {
this.testFeild = testFeild;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
public boolean getC() {
return c;
}
public void setC(boolean c) {
this.c = c;
}
}
3、运行阶段的注解——解析运行时注解并打印日志
a、新建注解@PrintLog
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
public @interface PrintLog {
String name();
}
b、创建测试类
@PrintLog(name = “printLog used for class”)
public class LogTest {
private int a;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
c、采用反射的方式获取对象注解并打印注解的name属性
public static void main(String[] args) {
LogTest a = new LogTest();
Class<?> logClass = a.getClass();
Annotation[] declaredAnnotations = logClass.getDeclaredAnnotations();
for (Annotation annotation : declaredAnnotations){
if (annotation.annotationType().equals(PrintLog.class)){
PrintLog printLog = (PrintLog)annotation;
System.out.println(printLog.name());
}
}
}
结果如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1vCmWVus-1639364489603)(file:///C:/Users/ZHOUZH~1/AppData/Local/Temp/msohtmlclip1/02/clip_image002.png)]
四、总结
介绍完注解相关的内容,回头看刚开始提出的三个问题进行总结
1、注解是一种接口,继承annotation接口的接口。根据注解创建和功能可分为元注解、JDK注解和第三方自定义注解,根据注解的生命周期,可以分为源码阶段注解、字节码阶段注解和运行时注解。
2、学会注解提高代码阅读能力、简化代码、降低耦合度等。对于源码和字节码阶段注解主要通过自定义注解处理器生效,对于运行时注解主要通过反射的方式进行解析。
3、注解处理器主要是采用类似xml文件解析的方式分析java源码,并在编译前后进行代码的处理。分别介绍源码、字节码以及运行时的注解并实现了相应的简单实例。
ring[] args) {
LogTest a = new LogTest();
Class<?> logClass = a.getClass();
Annotation[] declaredAnnotations = logClass.getDeclaredAnnotations();
for (Annotation annotation : declaredAnnotations){
if (annotation.annotationType().equals(PrintLog.class)){
PrintLog printLog = (PrintLog)annotation;
System.out.println(printLog.name());
}
}
}
结果如下图所示
[外链图片转存中…(img-1vCmWVus-1639364489603)]
四、总结
介绍完注解相关的内容,回头看刚开始提出的三个问题进行总结
1、注解是一种接口,继承annotation接口的接口。根据注解创建和功能可分为元注解、JDK注解和第三方自定义注解,根据注解的生命周期,可以分为源码阶段注解、字节码阶段注解和运行时注解。
2、学会注解提高代码阅读能力、简化代码、降低耦合度等。对于源码和字节码阶段注解主要通过自定义注解处理器生效,对于运行时注解主要通过反射的方式进行解析。
3、注解处理器主要是采用类似xml文件解析的方式分析java源码,并在编译前后进行代码的处理。分别介绍源码、字节码以及运行时的注解并实现了相应的简单实例。