你真的会用java注解吗?

Posted 一代小强

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的会用java注解吗?相关的知识,希望对你有一定的参考价值。

“揭开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:用于处理注解,生成代码的逻辑

接口

api模块定义了两个注解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(你真的会用java注解吗?

你真的会用Gson吗?Gson使用指南

你真的会用Gson吗?Gson使用指南

你真的会用Gson吗?Gson使用指南

你真的会用Gson吗?Gson使用指南

在C++中,你真的会用new吗?