注解

什么是注解

java.lang.annotation.Annotation 接口中有这么一句话,用来描述注解。

The common interface extended by all annotation types 所有的注解类型都继承 Annotation

这句话有点抽象,但却说出了注解的本质。我们看看 JDK 内置注解 @Override 的定义:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

其实本质上是:

java 复制代码
public interface Override extends Annotation{

}

没错,注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你可以去反编译任意一个注解类看看结果。

比如新建一个 Teacher.java 文件用于自定义注解:

less 复制代码
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Teacher {
    String name();
    int age() default 20; 
}

然后使用 javac Teacher.java 命令生成 Teacher.class 文件,再使用 javap Teacher.class 反编译后输出如下:

java 复制代码
Compiled from "Teacher.java"
public interface com.example.test.Teacher extends java.lang.annotation.Annotation {
    public abstract java.lang.String name();
    public abstract int age();
}

可以看到自定义注解确实是一个继承自 java.lang.annotation.Annotation 接口的接口。

准确意义上来说,注解只不过是代码里的特殊标记而已。这些标记可以在编译、类加载或运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、处理或者部署。

元注解

与声明一个类不同的是,注解的声明使用 @interface 关键字,@Override 注解的声明如下:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

其中的 @Target,@Retention 两个注解是我们所谓的『元注解』,『元注解』是用于修饰注解的注解,通常用在注解的定义上,『元注解』一般用于指定某个注解的保留策略以及作用目标等信息。

Java 中有以下几个『元注解』:

  • @Target:注解的作用目标
  • @Retention:注解的保留策略
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否允许子类继承该注解

其中,@Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的,修饰类的,还是用来修饰字段属性的。

@Target定义如下:

java 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

我们可以通过以下方式来为这个 value 传值:

@Target(value = {ElementType.FIELD})

上面这段代码表示该注解将只能作用在成员字段上,不能用于修饰方法或者类。其中,ElementType 是一个枚举类型,有以下一些值:

  • ElementType.TYPE 允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD 允许作用在成员变量上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造方法上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

@Retention 用于指明当前注解的保留策略,它的基本定义如下:

java 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
    RetentionPolicy value();
}

同样的,它也有一个 value 属性:

@Retention(value = RetentionPolicy.RUNTIME)

这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:

  • RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在 .java 源码中,源码在编译后,注解信息被丢弃,不会保留在 .class 中
  • RetentionPolicy.CLASS:编译时注解。注解信息会保留在 .java 源码以及 .class 中。当运行 Java 程序时, JVM 会丢弃该注解信息,不会保留在 JVM 中
  • RetentionPolicy.RUNTIME:运行时注解。当运行 Java 程序时, JVM 也会保留该注解信息,可以通过反射获取该注解信息

@Retention 注解指定了被修饰的注解的保留策略,这3个策略的生命周期长度为 SOURCE < CLASS < RUNTIME 。生命周期短的能起作用的地方,生命周期长的也一定能起作用。如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarning ,则可选用 RetentionPolicy.SOURCE 。如果要在编译时进行一些预处理操作,比如生成一些辅助代码(ARouter 的 @Route 注解就是这种情况),就用 RetentionPolicy.CLASS 。如果要在运行时动态获取注解信息,那就只能用 RetentionPolicy.RUNTIME 。

剩下两种类型的元注解我们日常用的不多,也比较简单,这里不再详细介绍了,你只需要知道他们各自的作用即可。@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

下面我们来自定义一个注解,里面有 2 个成员变量:

java 复制代码
@Target({ElementType.TYPE,ElementType.FIELD}) 
@Retention(RetentionPolicy.SOURCE)
public @interface Teacher {
    String name(); //无默认值
    int age() default 20; 
}

注解只有成员变量,成员变量在注解定义中以无形参的方法的形式来声明。在使用注解时,如果定义的注解中的成员变量无默认值,则使用时必须传值。

java 复制代码
@Teacher(name = "小帅") // name 无默认值,需要传值;age 有默认值,可以省略
@Teacher(name = "小帅", age = 22) // 也可以显式赋值 

源码级注解的应用

源码级注解的注解信息只会保留在 .java 源码中,编译后注解信息会被丢弃,不会保留在 .class 中。编译如下代码:

java 复制代码
@Teacher(name = "小帅", age = 22)
public class Test {
}

然后去 build\intermediates\javac\debug\classes\ 目录下找到 Test.class,打开发现已经找不到注解了。如果将保留策略改成 RetentionPolicy.CLASS ,则在 Test.class 中还可以找到注解:

class 复制代码
@Teacher(
    name = "小帅",
    age = 22
)
public class Test {
}

源码级注解主要用来做一些检查性的工作,比如,在 androidx.annotation 中有提供 @IntDef 注解,此注解的定义如下:

java 复制代码
@Retention(SOURCE) 
@Target({ANNOTATION_TYPE}) 
public @interface IntDef {
    int[] value() default {};
    boolean flag() default false;
    boolean open() default false;
}

此注解能够取代枚举,实现方法入参的限制。

Java 中的 enum (枚举)实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中,比常量多 5 到 10 倍的内存占用。

比如我们定义一个 test() 方法,接收的参数 Student 只能在 Jim 、 Lily 两位学生中选一个,如果使用枚举实现,代码如下:

java 复制代码
public class Test {

    enum Student{
        Jim,
        Lily
    }

    public void test(Student student){
    }
}

为了优化内存,我们现在不再使用枚举,改成如下代码:

java 复制代码
public class Test {

    public static final int Jim = 1;

    public static final int Lily = 2;

    public void test(int student){

    }
}

这种方式有什么弊端呢?test() 方法由于采用 int 类型,将无法限定为 Jim 和 Lily 。

下面我们使用注解进行优化:

java 复制代码
public interface Constants {
    int Jim = 1;
    int Lily = 2;
}
java 复制代码
@IntDef(value = {Constants.Jim, Constants.Lily})
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface Student {
}
java 复制代码
public class Test {

    public static void test(@Student int student){

    }
}

此时,我们再去调用 test() 方法,如果传递的参数不是 Constants.Jim 或 Constants.Lily ,Android Studio 会报红。

运行时注解的应用

针对运行时注解会采用反射机制获取注解信息,我们定义一个运行时注解,代码如下:

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Teacher {
    String name(); 
    int age() default 20;
}

接下来在 AnnotationTest 类的方法上应用该注解,代码如下:

java 复制代码
public class AnnotationTest {

    @Teacher(name = "老王")
    public String getMathTeacherName(){
        return "";
    }

    @Teacher(name = "老刘")
    public String getEnglishTeacherName(){
        return "";
    }
}

最后写一个简单的注解处理器,通过反射获取注解中的信息:

java 复制代码
public class AnnotationProcessor {

    public static void main(String[] args) {
        Method[] methods = AnnotationTest.class.getDeclaredMethods();
        for(Method m : methods){
            // 通过 getAnnotation() 获取指定类型的注解对象
            Teacher teacher = m.getAnnotation(Teacher.class);
            System.out.println(teacher.name());
        }
    }
}

输出结果为:

java 复制代码
老刘
老王

这样就拿到了注解中的成员变量的值,Retrofit 中的注解也是同样的原理,通过反射拿到注解中的信息。

编译时注解的应用

处理编译时注解相对会比较麻烦,编译时注解一般会结合注解处理器(APT)对注解进行处理,这里模拟一个 ButterKnife 自动绑定布局 id 的例子进行说明。

1. 定义注解

新建一个项目,在项目中新建一个 Java Library 来存放注解,Library 命名为 annotations 。接下来定义注解,如下:

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface MyBindView {
    // value 值对应要绑定的布局 id
    int value();
}

注解定义好了,在 Activity 中就可以使用了。在 app 模块的 MainActivity 中使用该注解,代码如下:

java 复制代码
public class MainActivity extends AppCompatActivity {

    @MyBindView(R.id.tv_text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

通过注解的 value 值就可以拿到布局 id ,但是怎么把 View 和 id 绑定起来呢?

如果 MyBindView 是 RetentionPolicy.RUNTIME 类型的,可以通过运行时反射绑定 id:

java 复制代码
public static void bind(Activity activity) {
    // 获取成员变量
    for (Field field : activity.getClass().getDeclaredFields()) {
        // 判断这个成员变量上是否有 @MyBindView 注解
        MyBindView myBindView = field.getAnnotation(MyBindView.class);
        if (myBindView != null) {
            try {
                // 将 activity 中成员 field 的值赋值为:activity.findViewById(myBindView.value())
                field.set(activity, activity.findViewById(myBindView.value()));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

但是现在注解不是 RetentionPolicy.RUNTIME 类型的,并且每次运行的时候去反射效率比较低,因为反射是比较耗性能的,当然你可以用缓存来优化,还有一种更好的办法是使用 APT 结合 JavaPoet 来实现。

2.编写注解处理器

再新建一个 Java Library 来存放注解处理器,Library 命名为 processor,接下来编写注解处理器:

java 复制代码
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 遍历所有的类
        for (Element element : roundEnvironment.getRootElements()) {
            // 获取类的包名
            String packageStr = element.getEnclosingElement().toString();
            // 获取类的名字
            String classStr = element.getSimpleName().toString();
            // 需要构建的新类名:原类名 + Binding
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
            // 构建新的类的构造方法
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");

            boolean hasBuild = false;
            // 获取类里面的元素,比如类的成员变量、方法、内部类等
            for (Element enclosedElement : element.getEnclosedElements()) {
                // 获取成员变量
                if (enclosedElement.getKind() == ElementKind.FIELD) {
                    // 判断是否被 @MyBindView 注解
                    MyBindView bindView = enclosedElement.getAnnotation(MyBindView.class);
                    if (bindView != null) {
                        // 需要生成类
                        hasBuild = true;
                        // 在构造方法中加入代码
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }
            // 判断需要生成
            if (hasBuild) {
                try {
                    // 构建新的类
                    TypeSpec builtClass = TypeSpec.classBuilder(className)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(constructorBuilder.build())
                            .build();
                    // 生成 Java 文件
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 只支持 @MyBindView 注解
        return Collections.singleton(MyBindView.class.getCanonicalName());
    }

}

在其 build.gradle 中添加如下配置:

c 复制代码
dependencies {
    implementation project(":annotations")
    // AutoService 是Google提供的
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
    // javapoet 是一个代码生成框架,利用它可以优雅的生成我们想要的代码
    implementation 'com.squareup:javapoet:1.10.0'
}

其中 AutoService 是 Google 提供的,用来帮助生成 javax.annotation.processing.Processor 文件的,如果你不使用它,你就需要自己去 resources 中添加该文件。

注解处理器里面的几个方法解释一下:

  1. init: 会被注解处理工具调用,入参为 ProcessingEnvironment,ProcessingEnvironment 提供了很多有用的工具类,比如 Elements、Types、Filter 和 Messager 等。
  2. process:你要在这里对注解进行处理。
  3. getSupportedAnnotationTypes:指定这个注解处理器支持的注解类型。

process() 方法中的代码会查找成员变量上有 @MyBindView 注解的类,生成代码,类名为:原类名 + Binding,并通过 findViewById 来绑定 View 和 id 。代码是通过 com.squareup:javapoet:1.10.0 这个框架来生成的,如果没有这个框架,那么需要使用字符串拼接的方式来生成代码, EventBus 就使用了字符串拼接的方式:

给 app 模块的build.gradle添加如下配置:

css 复制代码
dependencies {
    implementation project(path: ':annotations')
    annotationProcessor project(':processor')
}

然后在Terminal中输入:./gradlew :app:compileDebugJavaWithJavac,就会在 app 模块下生成MainActivityBind.java文件:

打开 MainActivityBind.java,代码如下:

java 复制代码
public class MainActivityBinding {
    public MainActivityBinding(MainActivity activity) {
        activity.textView = activity.findViewById(2131231011);
    }
}

此时只是生成了我们想要的代码,还需要调用 MainActivityBinding 的构造函数。

3.应用注解

再新建一个 Android Library,Library 命名为 reflect,在 build.gradle 中添加如下配置:

css 复制代码
dependencies {
    api project(path: ':annotations')
}

代码如下:

java 复制代码
public class MyButterKnife {
    public static void bind(Activity activity) {
        try {
            // 获取"当前的 activity 类名 + Binding "的 class 对象
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            // 获取 class 对象的构造方法,该构造方法的参数为当前的 activity 对象
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            // 创建实例
            constructor.newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最后在 app 模块的 MainActivity 中调用 MyButterKnife 的 bind() 方法,代码如下:

java 复制代码
public class MainActivity extends AppCompatActivity {
    @MyBindView(R.id.tv_text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyButterKnife.bind(this);
        textView.setText("Hello APT!!!!");
    }
}

其中 app 模块的 build.gradle 配置需要修改成如下所示:

css 复制代码
dependencies {
    implementation project(path: ':reflect')
    annotationProcessor project(':processor')
}

运行 MainActivity ,通过 MyButterKnife 的 bind() 方法就实现了自动绑定布局 id 的功能。可以分析一下 ARouter 框架,也是类似的原理。

Demo地址:github.com/EnzoXRay/An...

相关推荐
宇宙之大,无奇不有(一个玩暗区的人)1 天前
[NOIP 2011 普及组]T1 数字反转
java·开发语言·算法
技术小泽1 天前
搜索系统架构入门篇
java·后端·算法·搜索引擎
benpaodeDD1 天前
黑马springboot1
java·开发语言·spring boot
长安er1 天前
LeetCode121/55/45/763 贪心算法理论与经典题解析
java·数据结构·算法·leetcode·贪心算法·贪心
墨白曦煜1 天前
Lombok 速查指南:从基础注解到避坑实录
java
ss2731 天前
线程安全三剑客:无状态、加锁与CAS
java·jvm·数据库
The Sheep 20231 天前
可视化命中测试
java·服务器·前端
小小工匠1 天前
Vibe Coding - Claude Code 做 Java 项目 AI 结对编程最佳实践
java·结对编程·claude code
源码获取_wx:Fegn08951 天前
基于springboot + vue酒店预约系统
java·vue.js·spring boot·后端·spring