java注解(Annotation)总结
吐糟时间
想写博客也不是一天两天了,苦于各种原因,从来都没有写过,虽然平常印象笔记里记了一堆东西,但始终没有一个很好的总结,归根到底还是一个字,懒吧 ! 哎,真是怠惰啊~
作为不接触后台的android程序猿,前几天用了SpringBoot和各种组件后,发现这怎么这么多注解啊,喵喵喵???(不过话说回来,SpringBoot用起来真是爽啊,比ssh好用多了)
所以就回头看看注解的东西,顺便写上这篇博客~
注解是什么
@Override 这就是最常用的注解了~
@Override
public String toString() {
}
注解有什么用
- 标记,可以为编译器提供一些信息,以便于检测错误,抑制警告等(@Override、@SuppressWarnings)
- 编译时动态处理,如动态生成代码(android中的Retrofit、DataBinding、Dagger2、ButterKnife、EventBus3)
- 运行时动态处理,如得到注解信息(SpringBoot中的@AutoWired)
这里的三个作用实际对应着后面自定义 Annotation 时说的 @Retention 三种值分别表示的 Annotation
注解的分类
- 标准注解:Override, Deprecated, SuppressWarnings
指 Java 自带的几个 Annotation,上面三个分别表示重写函数,不鼓励使用(有更好方式、使用有风险或已不在维护),忽略某项 Warning - 元注解:@Retention, @Target, @Inherited, @Documented
元注解是定义注解的注解,在自定义注解的时候会用到 - 自定义注解
根据自己的需要,使用上面的元注解自定义注解
怎么自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
-
取一个你喜欢的名字,并使用@interface 声明
-
使用@Target 注解声明它会被使用的地方(类、属性、方法等)
取值(ElementType)有 1.CONSTRUCTOR:构造器 2.FIELD:成员变量 3.LOCAL_VARIABLE:局部变量 4.METHOD:方法 5.PACKAGE:包 6.PARAMETER:参数 7.TYPE:类、接口(包括注解类型) 或enum声明 注意,当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下: @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
-
使用@Retention 注解声明生命周期,即被描述的注解在什么范围内有效
1.SOURCE:在源文件中有效(即源文件保留,注解仅存在代码中,注解会被编译器丢弃) 2.CLASS:在class文件中有效(对应编译时注解,注解会在class文件中保留,但会被VM丢弃,在运行时期,这类注解是没有的) 3.RUNTIME:在运行时有效(对应运行时注解,VM运行期间也会保留该注解,因此可以通过反射来获得该注解) 注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等
除了这两种元注解外,还有 @Inherited 和 @Documented。
- @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解。
- @Documented 被修饰的注解会生成到javadoc中。
注意: 所有的Annotation会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
添加属性
自定义注解还会定义一些属性,在解析注解的时候使用到
@Target(ElementType.FIELD)//指定该注解使用的范围是成员变量
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {
String name();
String hero() default "";
int damage() default 0;
Constraints connstranints() default @Constraints;
}
注解的属性可支持数据类型:
-
所有基本数据类型(int,float,boolean,byte,double,char,long,short)
-
String类型
-
Class类型
-
enum类型
-
Annotation类型
-
以上所有类型的数组
注意: 1. 属性只能用public或默认(default)这两个访问权修饰。例如,String value();这里把方法设为defaul默认类型。 2. 如果一个注解的属性只有一个,且属性名是value(),那么使用注解的时候不需要写属性名字,直接赋值。如下面的 @Team注解。
注解的使用
@Target(ElementType.TYPE)//指定该注解使用的范围是类或者接口
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
String value() default "";
}
@Team("IG")
public class IG {
@Player(name = "the Shy", hero = "剑魔", damage = 28000, connstranints = @Constraints(isMvp = true))
private String top;
}
注意:
- 使用注解的时候,注解的每个属性都要赋值,有默认值的属性可以不赋。
- 自定义注解后,需要编写对应的解析类。
- 解析运行时注解和编译时注解的方法是不一样的。
- 解析运行时(RUNTIME)注解,必须通过Java的反射技术来获取 Annotation对象。
- 解析编译时(CLASS)注解,需要自定义一个Processor继承注解处理器 ( Annotation Processor )。
解析运行时(RUNTIME)注解
上面说了,我们写的注解都是自动继承Annotation接口的,要获取类方法,字段的注解信息,必须通过反射。在java.lang.reflect 反射包下有一个AnnotatedElement接口,通过这个接口提供的方法可以利用反射获取注解信息,反射包中的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口。
下面是AnnotatedElement中相关的API方法,以上5个类都实现以下的方法
返回值 | 方法名称 | 说明 |
---|---|---|
<A extends Annotation> | getAnnotation(Class<A> annotationClass) | 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。 |
Annotation[] | getAnnotations() | 返回此元素上存在的所有注解,包括从父类继承的 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false。 |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组 |
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {
String name() default "";
Constraints connstranints() default @Constraints;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean isMvp() default false;
boolean isDirector() default false;
}
@Team("IG")
public class IG {
@Player(name = "the Shy")
private String top;
@Player(name = "ning", connstranints = @Constraints(isMvp = true))
private String jungle;
@Player(name = "rookie")
private String mid;
@Player(name = "baolan")
private String support;
@Player(name = "jackeylove", connstranints = @Constraints(isDirector = true))
private String botLane;
@Player(name = "duke")
private String substitute;
}
public class Parse {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cl = Class.forName("com.test.lol.IG");
String team = cl.getAnnotation(Team.class).value();
StringBuilder teamPlayer = new StringBuilder();
for (int i = 0; i < cl.getDeclaredFields().length; i++) {
Field[] fields = cl.getDeclaredFields();
String fieldName = fields[i].getName();
Annotation[] anns = fields[i].getDeclaredAnnotations();
if (anns[0] instanceof Player) {
Player player = (Player) anns[0];
String name = player.name();
Constraints connstranints = player.connstranints();
boolean director = connstranints.isDirector();
boolean mvp = connstranints.isMvp();
teamPlayer.append(fieldName).append(":").append(name);
if (mvp)
teamPlayer.append("(MVP)");
if (director)
teamPlayer.append("(指挥)");
if (i != cl.getDeclaredFields().length - 1)
teamPlayer.append(",").append("\n");
}
}
String content = "2018英雄联盟S8总冠军:" + team + "\n"
+ "冠军成员:" + "\n"
+ teamPlayer;
System.out.println(content);
}
======================= 输出 =======================
2018英雄联盟S8总冠军:IG
冠军成员:
top:the Shy,
jungle:ning(MVP),
mid:rookie,
support:baolan,
botLane:jackeylove(指挥),
substitute:duke
解析编译时(CLASS)注解
自定义一个继承AbstractProcessor 的 Processor
public class Processor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
-
init()
在这里可以初始化一些工具类。
ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。
Elements:一个用来处理Element的工具类;
Types:一个用来处理TypeMirror的工具类;
Filer:正如这个名字所示,使用Filer你可以创建文件。 -
getSupportedAnnotationTypes()
这里你必须指定,这个注解处理器是注册给哪个注解的,并且是带包名+类名的全称。 -
getSupportedSourceVersion()
用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。 -
processor()
输入参数annotations 请求处理的注解类型集合。
输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素,相当于“有关全局源码的上下文环境”。
@return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.example.lib_annotation.BindView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyButterknifeProcessor extends AbstractProcessor {}
自定义Processor执行过程
上述的四个方法,前三个只会被调用一次,processor()方法可能会被调用多次,当没有输出文件也没有输入文件,处理结束。
用一个例子看一下 processor()调用的次数:
在看例子之前,先介绍几个会出现的演员,比较猴急的可以直接跳过
Element接口
Element在逻辑上代表语言元素,比如包,类,方法等
其实Element是定义的一个接口,定义了外部调用暴露出的接口
方法 | 解释 |
---|---|
TypeMirror asType() | 返回此元素定义的类型,实际的java类型(eg:String类型) |
ElementKind getKind() | 返回此元素的种类:包、类、接口、方法、字段…,如下枚举值,方法返回一个枚举值TypeKind |
Set getModifiers() | 返回此元素的修饰符,如下枚举值 |
Name getSimpleName() | 返回此元素的简单名称,比如activity名,变量就是变量名 |
Element getEnclosingElement() | 返回封装此元素的最里层元素,即最近的外层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null. |
List getEnclosedElements() | 获取所有的内层元素 |
< A extends Annotation> A getAnnotation(Class< A> var1) | 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的 |
Element子类
Element在逻辑上代表语言元素,比如包,类,方法等,因此也会有五个直接子接口,它们分别代表一种特定类型的元素
子类 | 解释 |
---|---|
PackageElement | 一个包程序元素 |
TypeElement | 一个类或接口程序元素 |
ExecutableElement | 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 |
TypeParameterElement | 一般类、接口、方法或构造方法元素的泛型参数 |
VariableElement | 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
TypeElement详解
TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象
方法 | 解释 |
---|---|
NestingKind getNestingKind(); | 返回此类型元素的嵌套种类 |
Name getQualifiedName(); | 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.譬如 Activity就是包名+类名 |
TypeMirror getSuperclass(); | 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType |
List getInterfaces(); | 返回直接由此类实现或直接由此接口扩展的接口类型 |
List getTypeParameters(); | 按照声明顺序返回此类型元素的形式类型参数 |
VariableElement详解
VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
方法 | 解释 |
---|---|
getConstantValue | 变量初始化的值 |
getEnclosingElement | 获取相关类信息 |
package apt;
//注解
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface Hello {
String name() default "";
}
//使用注解
package apt;
@Hello(name = "world")
public class Player {
}
//不使用注解
package apt;
public class Ignored {
}
package apt;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 源码级别, 这里的环境是 jdk 1.8
@SupportedAnnotationTypes("apt.Hello") // 处理的注解类型, 这里需要处理的是 apt 包下的 Hello 注解(这里也可以不用注解, 改成重写父类中对应的两个方法)
public class HelloProcessor extends AbstractProcessor {
// 计数器, 用于计算 process() 方法运行了几次
private int count = 1;
// 用于写文件
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
// 处理编译时注解的方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("start process, count = " + count++);
// 获得所有类
Set<? extends Element> rootElements = roundEnv.getRootElements();
System.out.println("all class:");
for (Element rootElement : rootElements) {
System.out.println(" " + rootElement.getSimpleName());
}
// 获得有注解的元素, 这里 Hello 只能修饰类, 所以只有类
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class);
System.out.println("annotated class:");
for (Element element : elementsAnnotatedWith) {
String className = element.getSimpleName().toString();
System.out.println(" " + className);
String output = element.getAnnotation(Hello.class).name();
// 产生的动态类的名字
String newClassName = className + "_New";
// 写 java 文件
createFile(newClassName, output);
}
return true;
}
private void createFile(String className, String output) {
StringBuilder cls = new StringBuilder();
cls.append("package apt;\n\npublic class ")
.append(className)
.append(" {\n public static void main(String[] args) {\n")
.append(" System.out.println(\"")
.append(output)
.append("\");\n }\n}");
try {
JavaFileObject sourceFile = filer.createSourceFile("apt." + className);
Writer writer = sourceFile.openWriter();
writer.write(cls.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在项目根目录新建一个out/production 目录
代码逻辑:
1、获得所有标有注解的类
2、取出注解中的信息
3、生成新的 java 文件
在idea的terminal里编译注解处理器
W:\workspace3\testAnnotation>javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java
接着执行注解处理器
W:\workspace3\testAnnotation>javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java
start process, count = 1
all class:
Hello
HelloProcessor
Ignored
Player
annotated class:
Player
start process, count = 2
all class:
Player_New
annotated class:
start process, count = 3
all class:
annotated class:
可以看到processor()的执行的次数为3次,当没有输入和输出后,不会再次执行。
过程 | 输入 | 输出 |
---|---|---|
第一次 | Hello.java , HelloProcessor.java ,Ignored.java, Player.java | Player_New.java |
第二次 | Player_New.java | - |
第三次 | - | - |
注意: 运行注解处理器的时候, 会开一个完整的 java 虚拟机执行代码, 所以自定义的注解处理器是可以使用各种类库的。
简单实现一个ButterKnife
好了,乱78遭的说了一堆,不管你懂没懂,DuangDuangDuang的看了一遍,然后自己动手来一遍就能大致理解了。
这个up的例子写的很好,也比较简单易懂,这里我就不再那啥了,毕竟贴图,贴代码还是很麻烦的…
从0到1:实现 Android 编译时注解 链接的文章结尾也有github的代码可以参考。
这里我还要提的是,关于调试 abstractProcessor的一点体会:
- debug 自定义的remote – apt的时候,有可能会碰上连接5005端口失败的错误,这个时候可以先执行assembleDebug,然后在debug apt,就可以连上5005端口了,接着执行assembleDebug,你会发现在abstractProcessor中打的断点就能调试了
- 用javaPoet成功生成文件以后,想再次进入abstractProcessor调试, 需要删除生成的MainActivity$$ViewInjector
- 调试的时候你会发现,processor()方法也一共被执行了3次,通过查看这两个参数的值,再结合上面也执行了3次的那个例子,想必聪明的你一定能懂。
-
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
=============================================================================