Java注解之编译时注解

AbstractProcessor

一个抽象类,实现注解处理器必须继承它,下面主要介绍里面的几个常用方法。

init(ProcessingEnvironment processingEnv)

初始化方法,用于获取一些有用的系统工具类,如Elements, Filer, Messager,Types等;

ProcessingEnvironment

初始方法作为参数传入,源码如下:

public interface ProcessingEnvironment {
    Map<String, String> getOptions();

    Messager getMessager();

    Filer getFiler();

    Elements getElementUtils();

    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}
  • getOptions:用来接收外部自定义参数的,后面getSupportedOptions方法中会介绍使用

  • getMessager:返回一个Messager对象,作为日志打印工具。看到有人说AnnotationProcessor是运行在javac期间,不能用System.out.print打印,其实System.out.print也是可以的,但是processingEnv.getMessager()功能更强大,可以区分Log类型,且Log Error类型时,会直接终止程序。

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    
        messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.WARNING, "TestAnnotationProcessor >>> warning");
        messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, "TestAnnotationProcessor >>> mandatory_warning");
        messager.printMessage(Diagnostic.Kind.NOTE, "TestAnnotationProcessor >>> note");
        messager.printMessage(Diagnostic.Kind.OTHER, "TestAnnotationProcessor >>> other");
        //messager.printMessage(Diagnostic.Kind.ERROR, "TestAnnotationProcessor >>> error"); //Error类型,会终止程序
    }
    

    执行Rebuild Project可以在Build窗口看到如下日志
    在这里插入图片描述

  • getFiler:返回一个Filer对象,主要负责生成文件。源码如下:

    public interface Filer {
        // 创建源文件
        JavaFileObject createSourceFile(CharSequence var1, Element... var2) throws IOException;
    
        // 创建class文件
        JavaFileObject createClassFile(CharSequence var1, Element... var2) throws IOException;
    
        // 创建资源文件
        FileObject createResource(Location var1, CharSequence var2, CharSequence var3, Element... var4) throws IOException;
    
        FileObject getResource(Location var1, CharSequence var2, CharSequence var3) throws IOException;
    }
    
  • getElementUtils:返回一个Elements对象,和Element相关的工具类。比如我们要获取包名怎么办?可以通过上面介绍过的getEnclosingElement方法一层一层网上找,非常麻烦也很容易出错。还可以通过Elements中的getPackageOf方法直接获取到

  • getTypeUtils:返回一个Types对象,和元素类型相关的工具类

  • getSourceVersion:支持的Java版本

  • getLocale:返回Locale对象,这个没什么可说的,就是国际化的东西

getSupportedOptions()

这个方法允许我们自定义一些参数传给Processor,就拿ARouter举例子

@Override
public Set<String> getSupportedOptions() {
    return new HashSet<String>() {{
        this.add(KEY_MODULE_NAME);
        this.add(KEY_GENERATE_DOC_NAME);
    }};
}

然后在gralde文件中的传一个参数

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

最后在Processor的init方法中获取参数

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    ...
    // Attempt to get user configuration [moduleName]
    Map<String, String> options = processingEnv.getOptions();
    if (MapUtils.isNotEmpty(options)) {
        moduleName = options.get(KEY_MODULE_NAME); 
    }
    ...
}
getSupportedSourceVersion()

设置支持的java版本,一般返回最近版本就行。

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

也可以使用@SupportedSourceVersion注解完成。

@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestProcessor extends AbstractProcessor {
	...
}
getSupportedAnnotationTypes()

设置支持的注解类型,只有在这个方法中添加过的注解才会被注解处理器所处理。

比如你自定义了个@TestAnnotation注解

@Override
public Set<String> getSupportedAnnotationTypes() {
    HashSet<String> set = new HashSet<>();
    set.add(TestAnnotation.class.getCanonicalName());
    return set;
}

也可以用@SupportedAnnotationTypes注解完成

@SupportedAnnotationTypes("com.wzc.annotation.TestAnnotation")
public class TestProcessor extends AbstractProcessor {
	...
}
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

所有关于注解的处理和java文件的生成都是在这个方法中完成,当返回值为true的时候表示这个Processor处理的注解不会再被后续的Processor处理。如果返回false,则表示这些注解还会被后续的Processor处理,类似拦截器模式。

参数

  • TypeElementSet:所有待处理的的注解集合。(一般用不上)
  • roundEnvironment:表示当前注解所处的环境,通过这个参数可以查询到当前这一轮注解处理的信息。主要使用roundEnvironment.getElementsAnnotatedWith(TestAnnotation.class)方法获取我们自定义注解标注的所有元素集合,是个Set<? extends Element>
Element

所有被注解标注的部分都会被解析成element,element既可能是类,也可能是类属性,还可能是方法,这就要看你使用自定义注解注解了那些东西了。获取到element之后我们还需要将element转换成对应的子类。

  • ExecutableElement: 可执行元素,包括类或者接口的方法。
  • PackageElement: 包元素
  • TypeElement:类,接口,或者枚举。
  • VariableElement: 类属性,枚举常量,方法参数,局部变量或者异常参数。
  • TypeParameterElement: 表示一个泛型元素
package com.wzc.gradle.myaptdemo; // PackageElement

// TypeElement
public class TestClass {
    // VariableElement
    private String mVariableElement;

    // ExecutableElement
    public TestClass(String mVariableElement // TypeElement) {
        this.mVariableElement = mVariableElement;
    }

    // ExecutableElement
    public static void main(String[] args //T ypeElement) {

    }
}

我们在定义注解的时候可以指定注解的ElementType,这个ElementType和Element是有对应关系的,通过测试可得到下面表格。

注解的ElementType注解处理器的Element
TYPETypeElement
FIELDVariableElement
METHODExecutableElement
PARAMETERVariableElement
CONSTRUCTORExecutableElement
LOCAL_VARIABLE获取不到
ANNOTATION_TYPETypeElement
PACKAGEPackageElement
TYPE_PARAMETERTypeParameterElement
TYPE_USE1对多,取决于使用的位置

拿到对应Element之后,还需要收集Element的相关信息,下面我们介绍几个常用的方法

  • getSimpleName:获取该元素的名字;
  • getModifiers:获取该元素的访问权限,返回一个Set;
  • asType: 获取该元素的类型,比如String会返回java.lang.String,TextView会返回android.widget.TextView;
  • getEnclosingElement:获取父级元素,比如参数的父级是方法,方法的父级是类或者接口;
  • getQualifiedName:获取全限定名,如果是类的话,包含完整的报名路径;

这些api大家可以自己去试一试。

接下来我们在process方法中来生成个简单的文件

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        for (Element element : roundEnvironment.getElementsAnnotatedWith(TestAnnotation.class)) {
            BufferedWriter bufferedWriter = null;
            try {
                VariableElement variableElement = (VariableElement) element;
                System.out.println("variableElement >>> getSimpleName = " + variableElement.getSimpleName());
                boolean kind = variableElement.getKind() == ElementKind.FIELD;
                System.out.println("variableElement >>> getKind = " + kind);
                System.out.println("variableElement >>> asType = " + variableElement.asType().toString());
                TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
                System.out.println("typeElement >>> getSimpleName = " + typeElement.getSimpleName());
                System.out.println("typeElement >>> getQualifiedName = " + typeElement.getQualifiedName());
                boolean typeElementKind = typeElement.getKind() == ElementKind.METHOD;
                System.out.println("typeElement >>> getKind = " + typeElementKind);
                System.out.println("typeElement >>> getModifiers = " + typeElement.getModifiers().toString());
                PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();
                System.out.println("typeElement >>> getSimpleName = " + packageElement.getSimpleName());
                System.out.println("typeElement >>> getQualifiedName = " + packageElement.getQualifiedName());
                boolean packageElementKind = packageElement.getKind() == ElementKind.CLASS;
                System.out.println("typeElement >>> getKind = " + packageElementKind);
                System.out.println("typeElement >>> getModifiers = " + packageElement.getModifiers().toString());
                JavaFileObject jfo = filer.createSourceFile("com.wzc.demo.HelloWorld");
                bufferedWriter = new BufferedWriter(jfo.openWriter());
                bufferedWriter.append("package ").append(packageElement.getQualifiedName()).append(";\n");
                bufferedWriter.append("public class ").append("HelloWorld {\n");
                bufferedWriter.newLine();
                bufferedWriter.append("public static void main(String[] args) {\n");
                bufferedWriter.newLine();
                bufferedWriter.append("System.out.println(\"Hello, World!\");\n");
                bufferedWriter.newLine();
                bufferedWriter.append("}");
                bufferedWriter.newLine();
                bufferedWriter.append("}");
                bufferedWriter.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (bufferedWriter != null) {
                    try {
                        bufferedWriter.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return false;
    }

推荐使用使用开源库JavaPoet来实现文件生成。

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

    try {
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();

        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();

        JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
                .build();

        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}

执行Rebuild Project我们可以在app\build\generated\ap_generated_sources\debug\out\文件夹下看到我们生成的类

实战

参考ARouter、butterknife项目结构都是

  • app :Demo
  • api:Android library,SDK对外暴露的api
  • annotations:java library,存放自定义注解
  • compiler:java library,存放注解处理器

我们也根据这个结构新建自己的项目
在这里插入图片描述
这里需要注意 Android libraryjava library的区别
自定义注解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    /**
     * 控件变量的resourceId
     */
    int value();
}

注解处理器
注解处理器library需要用到两个第三方库auto-service和javapoet

dependencies {
    implementation project(':findview-annotation')
    //使用AutoService注解,这里使用compileOnly就行,因为AutoService注解生命周期是在编译期的,具体可以看源码:https://github.com/google/auto/blob/master/service/annotations/src/main/java/com/google/auto/service/AutoService.java
    compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'
    //AutoService注解处理器用于自动为 JAVA Processor 生成 META-INF 信息。
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    //快速生成.java文件的库
    implementation 'com.squareup:javapoet:1.10.0'
}

auto-servic作用是帮我们自动生成 META-INF 信息,我们只有在我们注解处理器上加上如下代码:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

他就会帮我们生成如下文件
在这里插入图片描述
如果不使用AutoService,我们则需要自己libray中新建目录src/main/resources/META-INF/services,在该目录下下创建一个javax.annotation.processing.Processor文件,文件内写入你的注解处理器Processor的全类名。为了方便我们直接使用AutoService,替我们生成。

package com.wzc.findview.compiler;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.wzc.findview.annotation.BindView;

import java.io.IOException;
import java.util.HashSet;
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.Processor;
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.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    /**
     * 生成文件的工具类
     */
    private Filer filer;
    /**
     * 打印信息
     */
    private Messager messager;
    /**
     * 元素相关
     */
    private Elements elementUtils;
    private Types typeUtils;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            if (element.getKind() != ElementKind.FIELD) {
                break;
            }
            try {
                String variableName = element.getSimpleName().toString();
                int id = element.getAnnotation(BindView.class).value();
                TypeElement classElement = (TypeElement) element.getEnclosingElement();
                PackageElement packageElement = (PackageElement) classElement.getEnclosingElement();
                String className = classElement.getSimpleName() + "_ViewBinding";
                TypeName classTypeName = TypeName.get(classElement.asType());

                CodeBlock.Builder builder = CodeBlock.builder()
                        .add("target.$L = ", variableName);
                builder.add("target.findViewById($L)", id);
                
                MethodSpec main = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(classTypeName, "target")
                        .addStatement("$L", builder.build())
                        .build();
                TypeSpec bindClass = TypeSpec.classBuilder(className)
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(main)
                        .build();

                JavaFile javaFile = JavaFile.builder(packageElement.getQualifiedName().toString(), bindClass)
                        .build();

                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }
}

对外暴露的api

package com.wzc.findview.api;

import android.app.Activity;
import android.util.Log;

import java.lang.reflect.Constructor;

public class BindViewHelper {

    public static void bind(Activity activity){
        try {
            Class<?> clazz = activity.getClass().getClassLoader().loadClass(activity.getClass().getCanonicalName() + "_ViewBinding");
            Constructor constructor = clazz.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("wzc===", "Exception="+ e.getMessage());
        }
    }
}

使用

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tvTest)
    TextView mTvTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.getLocalClassName();
        BindViewHelper.bind(this);
        mTvTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "wwwww", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

追后注意下Java和Kotlin对注解的处理,使用的工具不一样。

  • Java 是使用AnnotationProcessor来根据注解来生成代码;
  • Kotlin 是使用Kapt 来根据注解来生成代码;

源码地址

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值