深入理解Java注解——编译时注解实战

Posted yubo_725

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Java注解——编译时注解实战相关的知识,希望对你有一定的参考价值。

在前面两篇博客中我记录了Java注解的一些知识点,以及如何使用JavaPoet生成Java源码,本篇主要记录的是使用Java编译时注解完成一个类似于ButterKnife的android View注入功能,通过注解即可完成View的ID绑定,不再显式调用findViewById方法。如果对Java注解还不太熟悉,可以参考我前面两篇博文:深入理解Java注解(一)——注解基础 | 深入理解Java注解(二)——JavaPoet使用

开始

本篇博客的代码还是基于深入理解Java注解(一)——注解基础 这篇博文中最后一部分“编译时注解”的代码,由于在深入理解Java注解(一)——注解基础 这篇博文中没有记录编译时注解的详细用法,仅仅是实现了一个空的注解处理器,它并未实现任何功能,故本篇中会实现一个类似ButterKnife的Android View注入框架,其使用方法如下:

  1. 在Activity中定义View并使用@MyAnnotation注解。

    public class MainActivity extends AppCompatActivity 
    
        @MyAnnotation(R.id.text_view)
        TextView textView;
    
        @MyAnnotation(R.id.image_view)
        ImageView imageView;
    
        ...
    
    
    
  2. build项目后,会生成与MainActivity对应的MainActivityViewInjector类,在MainActivityonCreate中调用MainActivityViewInjector.inject(this);即可完成View和ID的绑定。

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MainActivityViewInjector.inject(this);
        textView.setText("Hello Injector!");
        imageView.setImageResource(R.drawable.ic_launcher_background);
    
        textView.setOnClickListener(v -> toOtherActivity());
    
    
  3. 每个Activity都会生成对应的XXXViewInjector类,比如OtherActivity会生成OtherActivityViewInjector类。

实现步骤

定义注解

深入理解Java注解(一)——注解基础 这篇博文中已经定义了@MyAnnotation注解,由于我们需要将注解作用在Android的View上,且绑定其ID,ID为整型,故需要修改注解代码如下:

// 该注解作用在类的成员变量上
@Target(ElementType.FIELD)
// 该注解的保留策略为保留到字节码阶段
@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation 
    int value(); // 对应View的ID

实现注解处理器

深入理解Java注解(一)——注解基础 这篇博文中虽然定义了MyProcessor注解处理器,但并未有具体实现,这里为了实现本篇开头说的View注入功能,需要实现MyProcessor类的process方法,先放上注解处理器的全部代码:

package com.example.processor;

import com.example.annotation.MyAnnotation;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;

public class MyProcessor extends AbstractProcessor 

    // 打印日志用
    private Messager messager;
    // 操作文件用
    private Filer filer;

    // 针对MainActivity会生成MainActivityViewInjector类
    private static final String CLS_SUFFIX = "ViewInjector";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
    

    // 必须复写该方法,否则注解处理器不知道处理哪个注解
    @Override
    public Set<String> getSupportedAnnotationTypes() 
        // 返回支持的注解类型
        Set<String> types = new HashSet<>();
        types.add(MyAnnotation.class.getCanonicalName());
        return types;
    

    @Override
    public SourceVersion getSupportedSourceVersion() 
        // 返回支持的源代码版本
        return SourceVersion.latestSupported();
    

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
        // 注解处理主要在该方法中
        // elementsAnnotatedWith集合为所有被MyAnnotation注解的元素(Element represents a program element such as a package, class, or method)
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
        if (elementsAnnotatedWith.isEmpty()) 
            // 集合为空,不用继续处理
            return false;
        
        // map中的key代表某个类的全路径,value存放这个类中所有被注解标记了的元素
        Map<String, List<VariableElement>> map = new HashMap<>();
        for (Element e : elementsAnnotatedWith) 
            // kind表示元素类型
            ElementKind kind = e.getKind();
            // ElementKind.FIELD表示元素是一个字段
            if (kind == ElementKind.FIELD) 
                // VariableElement表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
                VariableElement variableElement = (VariableElement) e;
                String clsName = getFullClassName(variableElement);
                if (!map.containsKey(clsName)) 
                    map.put(clsName, new ArrayList<VariableElement>());
                
                map.get(clsName).add(variableElement);
                messager.printMessage(Diagnostic.Kind.NOTE, "add element " + e + " in class " + clsName);
            
        
        if (!map.isEmpty()) 
            Map<String, JavaFile> stringListMap = generateJavaCode(map);
            if (!stringListMap.isEmpty()) 
                // 将所有类对应的新生成的Java源码写入到文件
                Iterator<String> iterator = stringListMap.keySet().iterator();
                while (iterator.hasNext()) 
                    String clsName = iterator.next();
                    JavaFile javaFile = stringListMap.get(clsName);
                    try 
                        // 将生成的Java源码文件写入到filter,写入成功就会在项目主module的build/generated/ap_generated_sources/目录下生成源码文件
                        javaFile.writeTo(filer);
                     catch (IOException e) 
                        e.printStackTrace();
                    
                
            
        
        return true;
    

    // 使用JavaPoet生成新的Java代码
    private Map<String, JavaFile> generateJavaCode(Map<String, List<VariableElement>> map) 
        Iterator<String> iterator = map.keySet().iterator();
        Map<String, JavaFile> resultMap = new HashMap<>();
        while (iterator.hasNext()) 
            // 类名
            String clsName = iterator.next();
            // 类中被注解的字段集合
            List<VariableElement> list = map.get(clsName);
            // 包名
            String pkgName = getPackageName(list.get(0));
            // 构造一个静态的inject方法,在其中完成View的绑定
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("inject")
                    // 修饰器为public static
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    // 给方法添加参数,参数名为activity,参数类型是Activity
                    .addParameter(ClassName.get(pkgName, clsName), "activity");
            // 遍历类中所有被注解标记的元素
            for (VariableElement e : list) 
                // 拿到被注解标记的字段名称
                String fieldName = e.getSimpleName().toString();
                // 拿到注解中的值(即View的ID值)
                int value = e.getAnnotation(MyAnnotation.class).value();
                // 给上面构造的inject方法添加View赋值代码(activity.textView = activity.findViewById(xxx))
                methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", fieldName, value);
            
            // 每个类都生成对应的XXXViewInjector类
            String generatedClsName = getSimpleClassName(list.get(0)) + CLS_SUFFIX;
            // 构造XXXViewInjector类
            TypeSpec typeSpec = TypeSpec.classBuilder(generatedClsName)
                    // 类的修饰器为public
                    .addModifiers(Modifier.PUBLIC)
                    // 给这个类添加方法
                    .addMethod(methodSpecBuilder.build())
                    .build();
            // JavaFile表示一个Java源码文件
            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
            resultMap.put(generatedClsName, javaFile);
        
        return resultMap;
    

    // 获取元素的类名(不包含包名)
    private String getSimpleClassName(VariableElement element) 
        return ((TypeElement) element.getEnclosingElement()).getSimpleName().toString();
    

    // 获取元素所在的类名全路径(包括包名,比如com.example.testapt.MainActivity)
    private String getFullClassName(VariableElement element) 
        String packageName = getPackageName(element);
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        String className = typeElement.getSimpleName().toString();
        return packageName + "." + className;
    

    // 获取元素所在的包名
    private String getPackageName(VariableElement variableElement) 
        TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
        return processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
    

代码有点长,但是基本上每一行都做了注释,下面针对以上代码中的process方法做一些说明:

  1. Element是一个接口,它是程序元素的一个抽象,可以表示程序中的包、类、方法、字段等。它的源码中注释如下:Represents a program element such as a package, class, or method.
  2. 有很多类实现了Element接口,主要有如下几个:
说明
ExecutableElement表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素。
PackageElement表示一个包程序元素。提供对有关包及其成员的信息的访问。
TypeElement表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
TypeParameterElement表示一般类、接口、方法或构造方法元素的形式类型参数。
VariableElement表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
  1. 使用getKind()方法判断某个元素属于哪个类,比如e.getKind() == ElementKind.FIELD判断元素是否是一个字段。
  2. generateJavaCode方法中主要是使用JavaPoet生成了与某个Activity对应的XXXViewInjector类,如果对JavaPoet不熟悉,可以看下我的上一篇博文深入理解Java注解(二)——JavaPoet使用

验证

为了验证注解处理器是否正常工作,我们在AndroidStudio中点击菜单栏的Make Project图标,可以看到在Build视图中输出一些日志,如下图红色字体所示:

另外,在项目主module(我这里是app module)下的build/generated/ap_generated_sources/目录下,会生成对应的XXXViewInjector文件,如下图:

文件源码如下:

package com.example.testapt;

public class MainActivityViewInjector 
  public static void inject(com.example.testapt.MainActivity activity) 
    activity.textView = activity.findViewById(2131231104);
    activity.imageView = activity.findViewById(2131230909);
  

将项目跑在模拟器上,可以看到View注入能正常工作:

源码

本篇博客的源码放在GitHub上:https://github.com/yubo725/test-apt/tree/v0.2

参考

自定义Java注解处理器

以上是关于深入理解Java注解——编译时注解实战的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java注解——编译时注解实战

深入理解Java注解——注解基础

深入理解Java注解——注解基础

深入理解Java注解——注解基础

深入理解java虚拟机(17):插入式注解处理器实战

深入理解Java 注解原理