注解是在 Java SE5 引入进来的。注解在一定程度上是在把元数据与源代码结合在一起,而不是保存在外部文档中这一大的趋势之下所催生的。
因为笔者是作 Android 开发的,因此下面的介绍是偏于 Android 的实际应用。
本文会从以下几个方面来展开
@Retention
的不同取值到底有什么区别?这部分是本文的重点所在,会给出实例进行说明。注解有什么作用呢?或者说,我们为什么要学习注解?从实际开发中应用的注解来给出答案吧。
@Override
这个注解大家都使用过,它表示当前的方法定义将覆盖超类中的方法。如果不小心拼写错误,或者方法签名对不上被覆盖的方法,编译器就会发出错误提示。请看下面的例子:
public class MyRunnable implements Runnable {
// 这是正确的覆盖
@Override
public void run() {
}
// 这是错误的覆盖,但仍然使用了 @Override 注解
@Override
public void run2() {
}
}
使用 javac
命令进行编译,得到结果:
com\example\annotationstudy\MyRunnable.java:9: 错误: 方法不会覆盖或实现超类型的方法
@Override
^
1 个错误
其实,在 IDE 中可以看到第 9 行的 @Override
的地方底部有红色的波浪线,这是 IDE 进行的提示。这个提示和使用 javac
命令进行编译的提示是一模一样的。
需要说明的是,@Override
注解在 Java 中是可选择的,也就是说,可以写也可以不写。代码中第 4 行的 @Override
是可以不写的。但是,写上后可以增加程序的可读性,一眼就知道哪些方法是覆写的。
再举一个使用注解进行语法类型检查的例子:
package com.example.annotationstudy;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
public class Person {
@DrawableRes
private int avatarResId;
@StringRes
private int nameResId;
// @DrawableRes 注解表示期望这个 int 值是一个图片资源类的 id
// @StringRes 注解表示期望这个 int 值是一个 String 资源的 id
public Person(@DrawableRes int avatarResId, @StringRes int nameResId) {
this.avatarResId = avatarResId;
this.nameResId = nameResId;
}
}
在 PersonFactory
里创建一些 Person
对象:
public class PersonFactory {
public List<Person> createPersonList() {
List<Person> result = new ArrayList<>();
// 编译正确
result.add(new Person(R.mipmap.ic_launcher, R.string.name_peter));
// 在 IDE 中可以传入的实参都变成红色,编译出错,
// 参数 1 提示:Expected resource of type drawable
// 参数 2 提示:Expected resource of type string
result.add(new Person(1, 1));
return result;
}
}
这里的实例是 Android 中的 Room 数据库的使用。这里不再展开说明。可以参考文档:https://developer.android.google.cn/training/data-storage/room。
这里的实例是 Retrofit 中的注解,在运行时获取配置的请求方式,请求地址等信息。
通过给Annotation注解加上 @Documented
标签,能使该Annotation标签出现在 javadoc 中。
这几个作用里面,哪个最能吸引你呢?。
这里定义一个 @Test
注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
@Test
注解的定义很像一个空的接口。不同之处有哪些呢?
interface
前面必须加上 @
符号;@Target
和 @Retention
;正如接口可以没有任何方法一样(如 Serializable
接口),注解也可以没有任何元素,这样的注解被称为标记注解,例如上面定义的 @Test
,以及 内置的@Override
,@Deprecated
,@FunctionalInterface
,@SafeVarargs
注解。
Java 1.5 内置了一个有元素的注解:@SuppressWarnings
表示不当的编译器警告信息。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
什么是元注解呢?元注解就是专门负责注解其他注解的注解,或者说专门负责新注解创建的注解。
在 Java SE5 中有 4 种元注解:@Target
,@Retention
,@Documented
和 @Inherited
。
@Target
表示该注解可以用于什么地方。可能的 ElementType
参数包括:
CONSTRUCTOR
:用于构造器的声明;FIELD
:用于域声明(包括 enum
实例);LOCAL_VARIABLE
:用于局部变量的声明;METHOD
:用于方法的声明;PACKAGE
:用于包的声明;PARAMETER
:用于普通参数的声明;TYPE
:用于类、接口(包括注解类型)或 enum
声明;ANNOTATION_TYPE
:用于注解的声明;TYPE_PARAMETER
:这是 Java 1.8 加入的,用于类型参数的声明;TYPE_USE
:这是 Java 1.8 加入的,用于一个类型的使用。在 @Target
注解中指定的每一个 ElementType
就是一个约束,它告诉编译器,这个自定义的注解可以用于哪个类型或哪些类型。指定多个类型时,需要以逗号分隔并写在花括号{}
里面,例如 @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
。如果不写 @Target
注解,表示该注解可以用于所有的 ElementType
。
@Retention
表示需要在什么级别保留该注解信息。可选的 RetentionPolicy
参数包括:
SOURCE
:表示注解仅在源码中可用,将会被编译器丢掉;CLASS
:表示注解会被编译器记录在 class 文件中,但在运行时虚拟机(VM)不会保留注解。这也是默认的行为。RUNTIME
:表示注解会被编译器记录在 class 文件中,而且在运行时虚拟机(VM)会保留注解。所以这里可以通过反射读取注解的信息。可以参考:java.lang.reflect.AnnotatedElement
。注解是有限制的,具体来说如下:
int
、float
、boolean
等)String
Class
enum
Annotation
,这说明注解可以嵌套null
作为它的值。extends
来继承某个注解。但是,所有的注解类型都继承于通用的 Annotation
接口,而这一点是不能显式地写出来的。定义好注解后,就需要着手使用了。如果不使用,注解和注释也没有什么区别。要使用注解,重要的就是创建与使用注解处理器。如何创建与使用注解处理器和定义注解时指定的@Retention
有很大关系。
下面是一个非常简单的注解处理器,用来查找某个类中哪些成员使用了 @Deprecated
注解。
VideoItem
类是一个使用了 @Deprecated
注解的类:
public class VideoItem {
@Deprecated
private String objectId;
private String videoId;
private String videoUrl;
@Deprecated
public VideoItem(String objectId) {
this.objectId = objectId;
}
public VideoItem() {
}
@Deprecated
public String getObjectId() {
return objectId;
}
@Deprecated
public void setObjectId(String objectId) {
this.objectId = objectId;
}
}
看一下,@Deprecated
用在成员变量,构造方法,以及成员方法上。查看 @Deprecated
注解上指定的 @Retention
是 RetentionPolicy.RUNTIME
,表示可以通过反射读取注解的信息。
因此,我们可以使用反射机制来查找被@Deprecated
标记的成员,这样就知道了哪些成员是废弃的了。代码写在 DeprecatedTracker
里面:
public class DeprecatedTracker {
public static void trackDeprecated(Class<?> cl) {
// 查找成员变量
Field[] declaredFields = cl.getDeclaredFields();
for (Field field : declaredFields) {
if (field.getAnnotation(Deprecated.class) != null) {
System.out.println("deprecated field:" + field);
}
}
// 查找成员方法
Method[] declaredMethods = cl.getDeclaredMethods();
for (Method method : declaredMethods) {
if (method.isAnnotationPresent(Deprecated.class)) {
System.out.println("deprecated method:" + method);
}
}
// 查找构造方法
Constructor<?>[] declaredConstructors = cl.getDeclaredConstructors();
for (Constructor<?> constructor : declaredConstructors) {
if (constructor.isAnnotationPresent(Deprecated.class)) {
System.out.println("deprecated constructor:" + constructor);
}
}
}
public static void main(String[] args) {
DeprecatedTracker.trackDeprecated(VideoItem.class);
}
}
打印信息如下:
deprecated field:private java.lang.String com.example.annotationstudy.VideoItem.objectId
deprecated method:public java.lang.String com.example.annotationstudy.VideoItem.getObjectId()
deprecated method:public void com.example.annotationstudy.VideoItem.setObjectId(java.lang.String)
deprecated constructor:public com.example.annotationstudy.VideoItem(java.lang.String)
需要说明的是,上面例子中使用到的 getAnnotation()
和 isAnnotationPresent()
方法属于 AnnotatedElement
接口,而 Class
、Method
、Field
以及 Constructor
等都实现了该接口。getAnnotation()
方法返回指定类型的注解对象,如果元素没有指定该类型的注解,则返回 null
。isAnnotationPresent()
方法返回元素上是否有指定类型的注解。
上面是一个简单的例子,主要是为了快速展示注解处理器的创建与使用。下面我们会针对 @Target
的不同取值,分别给出实例。
@Target
为 SOURCE
的例子这里有两种应用,一种是进行语法检查,一种是使用 APT 技术生成代码。
语法检查的例子
现在有一个音乐列表中包含两种条目类型,一是音乐条目,一是原生广告条目。使用 MusicItem
来说明:
public class MusicItem {
public static final int ITEM_TYPE_MUSIC = 0;
public static final int ITEM_TYPE_AD = 1;
private int type;
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
在构造 MusicItem
对象的时候,可能有的同事会这样写:
public class MusicItemTest {
public static void main(String[] args) {
MusicItem musicItem = new MusicItem();
musicItem.setType(2);
}
}
注意,2
并不是合法的条目类型常量,但这时 IDE 并没有给出任何提示。
解决办法是定义 ItemType
注解,并在 getter/setter 方法上使用这个注解:
public class MusicItem {
@IntDef({ITEM_TYPE_MUSIC, ITEM_TYPE_AD})
@Retention(RetentionPolicy.SOURCE)
@interface ItemType {
}
public static final int ITEM_TYPE_MUSIC = 0;
public static final int ITEM_TYPE_AD = 1;
private int type;
public @ItemType int getType() {
return type;
}
public void setType(@ItemType int type) {
this.type = type;
}
}
然后可以看到在 musicItem.setType(2);
处已经有了语法检查(在 AS 4.1.1 中 2
完全显示红色),提示信息为:
Must be one of: MusicItem.ITEM_TYPE_MUSIC, MusicItem.ITEM_TYPE_AD
这个例子不是很复杂,但是有些地方需要做一下说明:
@IntDef
是由 Android 定义好的一个元注解,位置是在 androidx.annotation
包里面,表示被注解的整型元素代表了一个逻辑类型并且它的值应该是明显声明的常量。@IntDef
却是 @IntDef({ITEM_TYPE_MUSIC, ITEM_TYPE_AD})
,又不报错,这是什么原因呢?这其实是使用了快捷方式,即在注解中定义了名为 value
的元素,并且在用于该注解的时候,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名-值对这种语法,而只需在括号内给出 value
元素的值即可。在 androidx.annotation
包下,有许多帮助我们进行语法检查的注解,大家可以自行探索。这里我们不嫌啰嗦,再举一个 @IntRange
的使用。
在做分页请求时,有的后台给出页面从 0
开始,有的从 1
开始。这个必须要搞好,不然要么少么少请求一页数据,要么就啥也请求不到,给开发或者业务带来麻烦。
而使用 @IntRange
可以解决这个问题,进行后台接口接入的时候可以定义好是从 0
开始还是从 1
开始:
@IntRange(from = 1) int pageNumber = 0;
上面的声明就会语法检查不通过,提示:
Value must be ≥ 1 (was 0)
使用 APT 技术生成代码的例子
在实际开发中,页面中有大量控件时,去手动写出 findViewById()
的代码,是很累人的。不过,已经有很多优秀的框架,如 ButterKnife
,ViewBinding
可以解决这个问题。
这里我们实现一个类似于 ButterKnife
的方案。
不嫌啰嗦地,先看一下需要达到的目标,在一个页面 MainActivity
中不通过使用 findViewById()
的方式,获取到控件 id 对应的控件对象。最终的代码是:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView tv;
@BindView(R.id.iv)
ImageView iv;
@BindView(R.id.btn)
Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewManager.getInstance().bind(this);
tv.setText("Happy New Year!");
iv.setImageResource(R.mipmap.ic_launcher);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.start(MainActivity.this);
}
});
}
}
运行效果:
可以看到达到了我们的目标。接着,我们具体来看代码实现吧。
现在有了整体的结构,大家不要觉得很复杂。我们这里的代码量很小,只是为了结构清晰才分成了这些模块。
好了,我们开始查看每一个部分。
先看 bindview-annotations
模块,这里只是定义了一个 @BindView
的注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
@IdRes int value();
}
其中 @IdRes
是 androidx.annotation
包下的,所以需要添加这个依赖:
implementation 'androidx.annotation:annotation:1.1.0'
接着看 :bindview-compiler
模块,这是注解处理器模块,通过它会帮助我们完成 findViewById()
的工作。
这个模块的依赖是:
dependencies {
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
// 用于帮助我们通过类调用的形式来生成Java代码
implementation 'com.squareup:javapoet:1.9.0'
implementation project(':bindview-annotations')
}
核心代码如下:
// 这个注解的作用是用来生成 META-INF/javax.annotation.processing.Processor 这个文件,文件里就是
// 注解处理器的全路径,这个文件会被 ServiceLoader 类使用,用于加载注解服务。
@AutoService(Processor.class)
// 指定注解处理器支持的 JDK 编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 指定注解处理器支持处理的注解
@SupportedAnnotationTypes({ProcessorConstants.BINDVIEW_FULLNAME})
@SupportedOptions({ProcessorConstants.MODULE_NAME, ProcessorConstants.PACKAGENAME_FOR_APT})
public class BindViewProcessor extends AbstractProcessor {
/**
* 操作 Element 的工具类(类,函数,属性,枚举,构造方法都是 Element)
*/
private Elements elementUtils;
/**
* 打印日志类
*/
private Messager messager;
/**
* 用来对类型进行操作的实用工具方法
*/
private Types typeUtils;
/**
* 按 Activity 存放使用了 @BindView 注解的集合
* 键是 Activity
* 值是使用了 @BindView 的元素列表
*/
private Map<TypeElement, List<Element>> map = new HashMap<>();
/**
* 文件生成器
*/
private Filer filer;
private String moduleName;
private Map<String, String> options;
private String packagenameForAPT;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
typeUtils = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
options = processingEnvironment.getOptions();
parseOptions();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printMessage(Diagnostic.Kind.NOTE, "process: set=" + set);
if (!set.isEmpty()) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
if (!elements.isEmpty()) {
populateMap(elements);
if (map.isEmpty()) {
return true;
}
//public class MainActivity$$BindView implements BindViewInterface {
// @Override
// public void bind(Object target) {
// MainActivity activity = (MainActivity) target;
// activity.tv = activity.findViewById(R.id.tv);
// activity.cl = activity.findViewById(R.id.cl);
// }
//}
TypeElement activityType = elementUtils.getTypeElement(ProcessorConstants.ACTIVITY_FULLNAME);
TypeElement bindViewInterfaceType = elementUtils.getTypeElement(ProcessorConstants.BINDVIEWINTERFACE_FULLNAME);
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get("java.lang", "Object"), TARGET_ARGUMENT).build();
for (Map.Entry<TypeElement, List<Element>> entry : map.entrySet()) {
TypeElement key = entry.getKey();
if (!typeUtils.isSubtype(key.asType(), activityType.asType())) {
messager.printMessage(Diagnostic.Kind.ERROR,
"@BindView can only be annotated in Activity");
} else {
ClassName className = ClassName.get(key);
BindViewFactory bindViewFactory = new BindViewFactory.Builder(parameterSpec)
.className(className)
.elementUtils(elementUtils)
.messager(messager)
.typeUtils(typeUtils)
.build();
bindViewFactory.addFirstStatement();
List<Element> elementList = entry.getValue();
for (Element element : elementList) {
bindViewFactory.buildStatement(element);
}
MethodSpec methodSpec = bindViewFactory.build();
TypeSpec typeSpec = TypeSpec.classBuilder(key.getSimpleName() + "$$BindView")
.addModifiers(Modifier.PUBLIC)
.addMethod(methodSpec)
.addSuperinterface(ClassName.get(bindViewInterfaceType))
.build();
JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "create bindview file fail: " + e);
}
}
}
}
}
return false;
}
private void populateMap(Set<? extends Element> elements) {
for (Element element : elements) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
if (map.containsKey(enclosingElement)) {
map.get(enclosingElement).add(element);
} else {
ArrayList<Element> list = new ArrayList<>();
list.add(element);
map.put(enclosingElement, list);
}
}
}
private void parseOptions() {
moduleName = options.get(ProcessorConstants.MODULE_NAME);
packagenameForAPT = options.get(ProcessorConstants.PACKAGENAME_FOR_APT);
messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName +
",packagenameForAPT=" + packagenameForAPT);
if (moduleName != null && packagenameForAPT != null) {
messager.printMessage(Diagnostic.Kind.NOTE, "APT environment success");
} else {
messager.printMessage(Diagnostic.Kind.NOTE, "APT environment fail");
}
}
}
这时 Make Project,会在\bindview\build\generated\source\apt\debug\com\example\bindview 目录下生成 MainActivity$$BindView
文件:
public class MainActivity$$BindView implements BindViewInterface {
public void bind(Object target) {
MainActivity activity = (MainActivity) target;
activity.tv = activity.findViewById(2131231081);
activity.iv = activity.findViewById(2131230904);
activity.btn = activity.findViewById(2131230807);
}
}
可以看到在这里,进行了 findViewById()
的操作。接下来,就是如何使用这个文件了。
查看 bindview-api
模块,这里定义了生成文件的规则,即生成文件的接口,也定义了如何使用生成的文件,即 BindViewManager
类:
public class BindViewManager {
private static volatile BindViewManager instance = null;
private static final String BINDVIEW_FILE_NAME = "$$BindView";
private final LruCache<String, BindViewInterface> cache;
private BindViewManager() {
cache = new LruCache<>(100);
}
public static BindViewManager getInstance() {
if (instance == null) {
synchronized (BindViewManager.class) {
if (instance == null) {
instance = new BindViewManager();
}
}
}
return instance;
}
public void bind(Activity activity) {
String activityFullName = activity.getClass().getName();
BindViewInterface bindViewInterface = cache.get(activityFullName);
if (bindViewInterface == null) {
String bindViewInterfaceFileFullName = activityFullName + BINDVIEW_FILE_NAME;
try {
Class<?> clazz = Class.forName(bindViewInterfaceFileFullName);
bindViewInterface = (BindViewInterface) clazz.newInstance();
cache.put(activityFullName, bindViewInterface);
} catch (Exception e) {
e.printStackTrace();
}
}
if (bindViewInterface != null) {
bindViewInterface.bind(activity);
}
}
}
使用这个类的代码在 MainActivity
中了。
这里只是介绍了部分代码,详细代码可以查看文章末尾的代码链接。
回到我们的主题,这里我们定义了一个 @Target(SOURCE)
的 @BindView
注解,通过 apt 技术生成代码。那么,apt 是什么呢?为什么它可以帮助我们完成这样的工作?
apt 就是 annotation processing tool,注解处理工具,它专门用于操作 Java 源文件,而不是编译后的类。默认情况下,apt 会在处理完源文件后编译它们。
@Target
为 CLASS
的例子这部分的应用是字节码增强技术。暂时还没有研究这块。
@Target
为 RUNTIME
的例子这里仍然是使用 findViewById()
方法的例子,使用运行时注解来实现。
首先定义 @BindView
注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
@IdRes int value();
}
其次使用注解:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
然后编写注解处理器:
public class BindViewUtils {
public static void bind(Activity activity) {
Class<? extends Activity> activityClass = activity.getClass();
Field[] declaredFields = activityClass.getDeclaredFields();
for (Field field : declaredFields) {
BindView annotation = field.getAnnotation(BindView.class);
if (annotation != null) {
int id = annotation.value();
View view = activity.findViewById(id);
field.setAccessible(true);
try {
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
最后使用注解处理器:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewUtils.bind(this);
tv.setText("Hello, 2021!");
}
}
代码已经全部上传到 github 上,地址为 AnnotationStudy。