java注解详解

"使用java注解的正确姿势"

Posted by Weiwq on September 27, 2021

“揭开java注解的神秘面纱“

介绍

想必大家在接触java,甚至部分工作几年的,对于类、方法、字段上的 @xxx 都有一种迷茫:这是啥玩意,它是怎么运行起来的?

别慌,这就是java的注解,一个很常见但又神秘的特性。

我们从最熟悉的Override注解开始,Override对应的声明如下,可以看到,注解与接口的声明很相似,只不过多了一个@

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

同时他也依赖了其他两个注解Target和Retention。target的声明如下,用于声明注解的作用域,比如 Override是作用于方法的,如果在其他域使用该注解,编译器将会报错。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    // 注解值
    ElementType[] value();
}
// 注解类型
public enum ElementType {
   // 用于接口、类、枚举
    TYPE,
    // 字段和枚举常量
    FIELD,
    // 方法
    METHOD,
    // 参数
    PARAMETER,
    // 构造函数
    CONSTRUCTOR,
    // 局部变量
    LOCAL_VARIABLE,
    // 注解
    ANNOTATION_TYPE,
    // 包
    PACKAGE
}

而Retention的声明如下,其中CLASS、RUNTIME就是大名鼎鼎的编译时注解、运行时注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    // 返回注解策略
    RetentionPolicy value();
}
// 注解策略
public enum RetentionPolicy {
    // 注释将由编译器丢弃
    SOURCE,
    // 注释将由编译器记录在类文件中,但无需在运行时由 VM 保留,这是默认值行为。
    CLASS,
    // 注释将由编译器和运行时由 VM 保留,因此可以反射地读取它们
    RUNTIME
}

简单的来说,注解的声明有两个重要的注解:作用域(target)和保留策略(Retention)。其中保留策略很重要,它决定了注解的生命长度。

道理都懂,问题是注解怎么用,只是好(装)看(B)么?来,教你真功夫!

1、SOURCE注解

作用:source注解又称源码注解,给编译器读的,在编译成class文件的时候会被去掉,用于协助开发者编写正确的代码。

有如下代码,其中Override是源码注解,Test注解是编译时注解。

public class Main {
    private static class Parent {
        void read() {
            System.out.println("read");
        }
    }
    private static class Child extends Parent {
        @Override
        void read() {
            super.read();
        }
        @Test(id = 29)
        public void Test() {
        }
    }
}

对应的编译class文件如下,可以看到Override注解已经被移除,但是Test注解还在。

那SOUIRCE注解是怎么帮助编写正确的代码呢?

且看下面的例子:setLeve 方法需要限制传入的参数,只能传LEVE_1或者LEVE_2。我们可以通过定义Level 注解来实现。

import androidx.annotation.IntDef;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class Main {

    public static final int LEVE_1 = 1;
    public static final int LEVE_2 = 2;

    @Retention(RetentionPolicy.SOURCE) // 源码注解
    @Target(ElementType.PARAMETER) // 作用于参数
    @IntDef({LEVE_1, LEVE_2}) // 限制值的范围
    public @interface Level {
    }

    public static void main(String[] args) { 
        Main main = new Main();
        main.setLeve(0); // 报错
        main.setLeve(1); // 报错
        main.setLeve(LEVE_1); // 正确
    }
    // 限制合法参数为LEVEL_1 和 LEVEL_2
    public void setLeve(@Level int level) {
        System.out.println("level " + level); 
    }
}

一般如果要实现上述需求,需要定义对应的枚举来实现,这里通过Android 提供的IntDef 注解,定义对应参数的值范围,达到枚举的效果,并且性能比枚举好。

2、运行时注解

作用:保留到运行阶段。主要在代码执行的时候会获取该注解 ,做一些反射的操作。

我们用运行时注解实现butterKnife的功能,核心思路:通过遍历指定的注解,拿到值后,用activity的方法获取view,再反射绑定到对应的属性上。

接口

首先定义两个注解接口

// 用于绑定view
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FindView {
    int value();
}

// 用于绑定方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int value();
}

处理器

定义ViewProcessor,用于在运行时解析注解。

public class ViewProcessor {
    private static final String TAG = "ViewProcessor";

    public void inject(Activity activity) {
        try {
            injectId(activity);
            injectOnClick(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void injectOnClick(Activity activity) {
        Class<?> cls = activity.getClass();
        // 获取全部声明的方法
        for (Method method : cls.getDeclaredMethods()) {
            Log.d(TAG, "injectOnClick method is : " + method.getName());
            // 获取该方法上的注解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (!(annotation instanceof OnClick)) {
                    continue;
                }
                // 找到OnClick注解
                OnClick findView = (OnClick) annotation;
                // 获取OnClick的值
                int id = findView.value();
                // 找到对应的view
                View view = activity.findViewById(id);
                if (view == null) {
                    continue;
                }
                view.setOnClickListener((view1) -> {
                    Log.d(TAG, "injectOnClick: callback");
                    try {
                        // 反射调用该方法
                        method.setAccessible(true);
                        method.invoke(activity);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }

    private void injectId(Activity activity) throws IllegalAccessException {
        Class<?> cls = activity.getClass();
        for (Field field : cls.getDeclaredFields()) {
            Log.d(TAG, "injectOnClick filed is : " + field);
            Annotation[] annotations = field.getAnnotations();
            for (Annotation annotation : annotations) {
                if (!(annotation instanceof FindView)) {
                    continue;
                }
                // 找到FindView注解
                FindView findView = (FindView) annotation;
                int id = findView.value();
                View view = activity.findViewById(id);
                if (view == null) {
                    continue;
                }
                field.setAccessible(true);
                // 给该域赋值
                field.set(activity, view);
            }
        }
    }
}

使用

在onCreate的时候,初始化注解处理器,实现注解的解析。

public class RuntimeActivity extends AppCompatActivity {

    @FindView(R.id.runtime_button1)
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_runtime);
        // 初始化注解处理器
        ViewProcessor bindViewHelper = new ViewProcessor();
        bindViewHelper.inject(this);
        mButton.setOnClickListener((view) -> {
            Toast.makeText(this, "运行时注解 FindView", Toast.LENGTH_SHORT).show();
        });
    }

    @OnClick(R.id.runtime_button2)
    private void onClick2() {
        Toast.makeText(this, "运行时注解 OnClick", Toast.LENGTH_SHORT).show();
    }
}

小结

  • 优点:通过反射方式,实现赋值和方法调用,对于域或方法的访问范围不做要求,框架实现较为简单。
  • 缺点:使用大量反射,运行时性能较差。

3、编译时注解——概念

作用:在编译期间生效的,常用于在编译期间插入模板代码。

什么是APT

这里不得不提一下APT,APT(Annotation Processing Tool)是 javac 提供的一种可以处理注解的工具,用来在编译时扫描和处理注解的,简单来说就是可以通过 APT 获取到注解及其注解所在位置的信息,可以使用这些信息在编译器生成代码。编译时注解就是通过 APT 来通过注解信息生成代码来完成某些功能,典型代表有 ButterKnife、Dagger等。

AbstractProcessor

AbstractProcessor 是实现编译注解的关键入口,自定义的注解处理器都是需要继承于它,其中以下方法比较重要:

  • init:主要做一些初始化的动作,比如Elements、Filer 和 Message等。
  • getSupportedAnnotationTypes:用来设置支持的注解类型
  • getSupportedSourceVersion:获取java版本。
  • process:解析注解,生成代码模板的实现回调。

Element

Element 用于表示程序元素,例如模块、包、类或方法。每个元素代表一个静态的、语言级别的构造。而Elements 是处理 Element 的工具类,只提供接口。

4、编译时注解——实现

我们用编译时注解重写一下上面的butterKnife。

项目中,有如下module

  • app:用于demo演示
  • api:用于定义注解,比如BindView
  • butterKnife:用于处理注解,生成代码的逻辑

接口

该模块定义了两个注解BindView和Onclick


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Onclick {
    int[] value();
}

处理器

butterKnife 模块需要依赖第三方库

dependencies {
    // 用来生成META-INF/services/javax.annotation.processing.Processor文件
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    // 用于创建Java文件
    implementation 'com.squareup:javapoet:1.12.1'
    // 导入javaX包
    targetCompatibility = '1.8'
    sourceCompatibility = '1.8'
}

对应的注解处理器是 ButterKnifeProcessor


// 用于声明该类为注解处理器
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    // 用于打印日志信息
    private Messager mMessager;
    // 用于解析 Element
    private Elements mElements;
    // 存储每个类下面对应的BindView
    private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();
    // 存储m每个类中,id绑定的方法,即OnClick
    private Map<TypeElement, Map<Integer, Element>> mOnclickElementMap = new HashMap<>();
    // 用于将创建的java程序输出到相关路径下。
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mMessager = processingEnv.getMessager();
        mElements = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
    }

    /**
     * 此方法用来设置支持的注解类型,没有设置的无效(获取不到)
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        // 把支持的类型添加进去
        supportTypes.add(BindView.class.getCanonicalName());
        supportTypes.add(Onclick.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "===============process start =============");
        mTypeElementMap.clear();
        // 解析 @BindView element.
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            verifyAnnotation(element, BindView.class, ElementKind.FIELD);
            // 可以理解为类的element
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            // 获取class 完整name,比如:com.example.annotation.buildtime.MainActivity
            Name qualifiedName = enclosingElement.getQualifiedName();
            // 获取变量名,比如 button1
            Name simpleName = element.getSimpleName();

            //获取到view的id
            int id = element.getAnnotation(BindView.class).value();
            String content = String.format("====> qualifiedName: %s simpleName: %s id: %d"
                    , qualifiedName, simpleName, id);
            mMessager.printMessage(Diagnostic.Kind.NOTE, content);
            List<BindModel> modelList = mTypeElementMap.get(enclosingElement);
            if (modelList == null) {
                // 每个activity会有多个BindView注解
                modelList = new ArrayList<>();
            }
            modelList.add(new BindModel(element, id));
            mTypeElementMap.put(enclosingElement, modelList);
        }
        // 解析 @Onclick element.
        for (Element element : roundEnv.getElementsAnnotatedWith(Onclick.class)) {
            verifyAnnotation(element, Onclick.class, ElementKind.METHOD);
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            Map<Integer, Element> methods = mOnclickElementMap.get(enclosingElement);
            if (methods == null) {
                // 每个类会有多个Onclick注解
                methods = new HashMap<>();
            }
            int[] ids = element.getAnnotation(Onclick.class).value();
            for (int id : ids) {
                // 将id与方法绑定
                methods.put(id, element);
            }
            // 将methods 与类绑定
            mOnclickElementMap.put(enclosingElement, methods);
        }
        // 遍历类
        mTypeElementMap.forEach((typeElement, bindModels) -> {
            // 获取包名
            String packageName = mElements.getPackageOf(typeElement)
                    .getQualifiedName().toString();
            String className = typeElement.getSimpleName().toString();
            // 生成对应的_ViewBind 类名
            String bindClass = className + "_ViewBind";
            // 生成构造函数
            MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC) // 声明为public
                    .addParameter(ClassName.bestGuess(className), "target"); // 添加构造参数
            bindModels.forEach(model -> {
                // 构造函数内添加findViewById代码
                builder.addStatement("target.$L = ($L)target.findViewById($L)",
                        model.getViewFieldName(), model.getViewFieldType(), model.getResId());
            });
            String viewPath = "android.view.View";
            Map<Integer, Element> clickMethods = mOnclickElementMap.get(typeElement);
            if (clickMethods != null) {
                clickMethods.forEach((id, element) -> {
                    // 构造函数内添加setOnClickListener代码
                    builder.addStatement("(($L) target.findViewById($L)).setOnClickListener((view) -> {\n" +
                                    "            target.$L();\n" +
                                    "        })",
                            viewPath, id, element.getSimpleName().toString());
                });
            }
            // 构建类
            TypeSpec typeSpec = TypeSpec.classBuilder(bindClass)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(builder.build())
                    .build();
            // 生成java file
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                    .addFileComment("auto create by ButterKnife ")
                    .build();
            try {
                // javaFile 写到指定路径下
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        mMessager.printMessage(Diagnostic.Kind.NOTE, "===============process end=============");
        return true;
    }

    // 做校验动作
    private boolean verifyAnnotation(Element element, Class<?> annotationClass, ElementKind targetKind) {
        if (element.getKind() != targetKind) {
            error(element, "%s must be declared on field.", annotationClass.getSimpleName());
            return false;
        }
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
            error(element, " %s %s must not be private or static.",
                    annotationClass.getSimpleName(),
                    element.getSimpleName());
            return false;
        }
        return true;
    }

    /**
     * 打印错误日志方法
     */
    private void error(Element element, String message, Object... args) {
        if (args.length > 0) {
            message = String.format(message, args);
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE, message, element);
    }
}

上面的代码大家可能会不知所措,其通过JavaPoet 来声明生成的java文件结构。具体的用法可以参考JavaPoet使用详解

BindModel 类是用于存储Element数据。方便代码的生成。

public class BindModel {
    // 成员变量Element
    private VariableElement mViewFieldElement;
    // 成员变量类型
    private TypeMirror mViewFieldType;
    // View的资源Id
    private int mResId;

    public BindModel(Element element, int resId) {
        // 校验Element是否是成员变量
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException("element is not FIELD");
        }
        // 成员变量Element
        mViewFieldElement = (VariableElement) element;
        // 成员变量类型
        mViewFieldType = element.asType();
        // 获取注解的值
        mResId = resId;
    }

    public int getResId() {
        return mResId;
    }
    ....
}

使用

app模块主要依赖

android {
    defaultConfig {
        // 声明注解器
        javaCompileOptions {
            annotationProcessorOptions {
                includeCompileClasspath true
                classNames = ["com.example.compiler.ButterKnifeProcessor"]
            }
        }
    }
}
dependencies {
    // 注意,是通过 annotationProcessor方式依赖butterKnife 而不是implementation。
    annotationProcessor project(path: ':butterKnife')
    implementation project(path: ':api')
}

还有一个问题需要解决的,ButterKnifeProcessor 为我们定义了代码模板,但是需要一个类将activity或者fragment与模板绑定,即ViewInjector:


public class ViewInjector {
    private static final String SUFFIX = "_ViewBind";
    public static void injectView(Activity activity) {
        findProxyActivity(activity);
    }
    /**
     * 通过反射创建要使用的类的对象
     */
    private static void findProxyActivity(Object activity) {
        try {
            Class<?> clazz = activity.getClass();
            String newClass = clazz.getName() + SUFFIX;
            Class<?> injectorClazz = Class.forName(newClass);
            injectorClazz.getConstructor(clazz).newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后就可以使用注解替换对应的代码了

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.button1)
    public Button button1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.injectView(this);
        button1.setOnClickListener((view) -> {
            Toast.makeText(this, "编译时:点我1", Toast.LENGTH_SHORT).show();
        });
    }

    @Onclick(R.id.button2)
    public void press1() {
        Toast.makeText(this, "编译时: 点我2", Toast.LENGTH_SHORT).show();
    }
}

点击build后,在build/generated/ap_generated_sources/debug/out/com/example/annotation/路径下,生成对应的代码

public final class MainActivity_ViewBind {
    public MainActivity_ViewBind(MainActivity target) {
        target.button1 = (android.widget.Button) target.findViewById(2131230812);
        ((android.view.View) target.findViewById(2131230813)).setOnClickListener((view) -> {
            target.press1();
        });
    }
}

小结

  • 优点:通过插入编译期间,生成模板代码,即java文件,避免了运行时注解中使用反射的实现方式,提高运行时效率。
  • 缺点:域或方法的访问范围必须是public,否则就会失败。

后记

上面没有具体分析butterKnife的源码,但是其原理与上面写的Demo有异曲同工之妙。也许我们不一定要自己造轮子,但应该需要知道对应的基础实现原理,这样我们才能透过现象看本质。

附上源码github链接

——Weiwq 于 2021.09 广州