Java编译时注解处理器的学习总结

APT的简介

定义

APT即是Annotation Processing Tool ,它是一个javac的一个工具,中文的意思是编译时注解处理器,可以用来在编译时扫描和处理注解。通过APT可以取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动生成一些代码,省去了手动编写。获取注解及生成代码都是在代码编译的时候完成的,相比反射在运行时处理注解大大提高了程序性能。APT的核心就是AbstractProcessor类。

如何在Android中使用APT呢

在Android工程中使用APT,至少需要两个Java Library模块组成,在Android中创建Java Library的的步骤是,首先建一个Android项目,然后点击File-> New ->New Module,打开如图所示,然后选择Java Library模块。


image.png

这两个模块的作用是:
一个Annotation模块,这个用来存在自定义的注解。
一个Compiler模块,这个模块依赖Annotation模块。
在项目的App模块和其它的业务模块都需要依赖Annotation模块,同时需要通过annotationProcessor依赖Compiler模块。
app模块的gradle中依赖关系如下:

implementation project(':annotation')
annotationProcessor project(':factory-compiler')

实现ButterKnife例子学习APT

步骤:

新建一个Android的app的工程。
创建一个Java Library ,定义要被处理的注解(apt-annotation)。
创建一个Java Library,定义注解处理器生成具体的类。(apt-processor)
创建一个Android library module,通过反射调用apt-processor模块生成的方法,实现View的绑定。

工程目录如下:


image.png
在apt-annotation中定义一个注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value() default -1;
}

因为这个注解是在成员变量中使用的,保留的时间是在.class字节码文件,不需要值JVM期间保留,这也体现了APT只是在编译器的编译时期完成工作。

在apt-processor中的gradle文件中加入两个依赖。

为了解决android studio升级到3.4.2,gradle升级到5.1.1后,apt不会执行,没办法自动生成注解文件的问题,还需要添加 annotationProcessor。

  implementation project(':apt-annotation')
  implementation 'com.google.auto.service:auto-service:1.0-rc4'
 annotationProcessor  'com.google.auto.service:auto-service:1.0-rc4'

第二个依赖是AutoService,它是google开发的一个库,在使用@AutoService注解时需要用到,它的作用是用来生成META.INF/services/javax.annotation.processing.Processor文件的,在使用注解器的时候就不需要手动添加该文件。

在apt-processor中创建注解处理器

该Module是Java Library,不能是Android Library,因为Android平台的是基于OpenJDK的,而OpenJDK中是不包含APT的相关代码。

BindViewProcessor要引用AutoService的注解,以及继承AbstractProcessor.并重写相应的方法:

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

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

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

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

接下来对重写的方法进行解析

  • public synchronized void init()
    这个 方法是用来初始化处理器的,方法中有个ProcessingEnvironment processingEnvironment类型的参数,它是一个注解处理工具的集合。包含了众多的工具类,例如Filer可以用来编写新文件。Meessage可以用来处理错误信息。Elements是一个可以处理Element的工具类。
  • 什么是Elments?
    在Java语言中,Elenent是一个接口,表示一个程序的元素,例如包,类,方法,变量。Element已知的子接口有如下几种。
    PackageElement 表示一个包程序元素。
    ExecutableElement表示某个类或者接口的方法,构造方法和初始化程序,包括注释类型元素。
    TypeElement 表示一个类或者接口的元素。注意对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
    VariableElement表示一个字段,enum常量,方法或者构成方法参数,局部变量或者异常参数。
  • public boolean process()
    这个是AbstractProcessor中最重要的一个方法,该方法的返回值是一个boolean类型,返回值表示注解是否由当前Processor处理,如果返回true,则这些注解由这个处理器进行处理,后续的其他Processor无需处理它们。如果返回false,则这些注解未在次Processor中处理,需要后续的其他Processor处理它们。在这个方法中,我们需要检验被注解的对象是否合法,可以编写处理注解的代码,以及自动生成需要的java文件等。处理的大部分逻辑都在这个类中。
  • public Set<String> getSupportedAnnotationTypes()
    这方法是返回一个set集合,集合中指定那些注解是需要这个处理器处理的。
  • public SourceVersion getSupportedSourceVersion()
    这个返回是返回当前的正在使用的java版本。通常返回SourceVersion.latestSupported()就可以了。

接下来是编写BingViewProcessor的代码,注意改文件中不可以含有中文,否则编译不通过。代码中的中文是为了方便理解加的解析,在运行的时候要删除。如果需要中文解析的可以在build.gradle中添加

//中文乱码问题(错误:编码GBK不可映射字符)
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Elements mElementUtil;
    private Map<String,ClassCreatorFactory> mClassCreatorFactoryMap=new HashMap<>();
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //初始化Element
        mElementUtil= processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mClassCreatorFactoryMap.clear();
        //获取所有包含了@BingView注解的集合
        Set<? extends Element> elements=roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element:elements){
            VariableElement variableElement= (VariableElement) element;
            TypeElement typeElement= (TypeElement) variableElement.getEnclosingElement();
            String fullClassName=typeElement.getQualifiedName().toString();
            ClassCreatorFactory classCreatorFactory=mClassCreatorFactoryMap.get(fullClassName);
            if (classCreatorFactory==null){
                classCreatorFactory=new ClassCreatorFactory( mElementUtil,typeElement);
                mClassCreatorFactoryMap.put(fullClassName,classCreatorFactory);
            }
            BindView bindViewAnnotation=variableElement.getAnnotation(BindView.class);
            int id=bindViewAnnotation.value();
            classCreatorFactory.putElement(id,variableElement);
}
   //开始创建Java类
            for (String key:mClassCreatorFactoryMap.keySet()){
                ClassCreatorFactory factory=mClassCreatorFactoryMap.get(key);
                try {
                    JavaFileObject fileObject=processingEnv.getFiler().createSourceFile(factory.getClassFullName(),factory.getTypeElement());
                    Writer writer=fileObject.openWriter();
                    writer.write(factory.generateJavaCode());
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        //返回true说明这个processor要处理这个注解,后续的Processor不需
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //设置这个注解器是给哪个注解类用的,这里是给BingView的注解类使用
        HashSet<String> supportType=new LinkedHashSet<>();
        supportType.add(BindView.class.getCanonicalName());
        return supportType;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //返回java的版本
        return SourceVersion.latestSupported();
    }

}

ClassCreatorFactory的代码如下,注意该类的代码不能出现中文的注释,该类的代码就是相当于在代码中通过字符串的方式编写代码,需要注意空格等细节问题,这个类很容易出错,可以利用JavaPoet根据实体类生成。

public class ClassCreatorFactory {
    private String mClassName;
    private String mPackageName;
    private TypeElement typeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public ClassCreatorFactory(Elements elements, TypeElement typeElement) {
        //获取该类
        this.typeElement = typeElement;
        //获取包元素
         PackageElement packageElement = elements.getPackageOf(typeElement);
        mPackageName = packageElement.getQualifiedName().toString();
        String temClassName = typeElement.getSimpleName().toString();
        //生成类的名称
        mClassName = temClassName + "_ViewBinding";
    }

    /**
     * 添加VariableElement,例如字段,局部变量,参数等
     * @param id
     * @param element
     */
    public void putElement(int id,VariableElement element){
        mVariableElementMap.put(id,element);
    }

    /**
     * 创建java代码,可以看出是文本的形式,对字符进行拼接即可。
     * @return
     */
    public String generateJavaCode(){
        StringBuilder stringBuilder=new StringBuilder();
        //注释
        stringBuilder.append("/**\n"+"* 通过APT自动创建的类"+"\n*/\n");
        //包名
        stringBuilder.append("package ").append(mPackageName).append(";\n\n");
        stringBuilder.append("public class ").append(mClassName).append("{\n");
        //创建方法
        generateMethod(stringBuilder);
        stringBuilder.append("\n}\n");
        return stringBuilder.toString();
    }

    /**
     * 创建方法
     * @param stringBuilder
     */
    private void generateMethod(StringBuilder stringBuilder) {
        stringBuilder.append("\tpublic void bindView(");
        stringBuilder.append(typeElement.getQualifiedName());
        stringBuilder.append(" value) { \n");
        for (int id:mVariableElementMap.keySet()){
            VariableElement variableElement=mVariableElementMap.get(id);
            String viewName=variableElement.getSimpleName().toString();
            String viewType=variableElement.asType().toString();
            stringBuilder.append("\t\tvalue.");
            stringBuilder.append(viewName);
            stringBuilder.append(" = ");
            stringBuilder.append("(");
            stringBuilder.append(viewType);
            //findViewById(id);
            stringBuilder.append(")(((android.app.Activity)value).findViewById( ");
            stringBuilder.append(id+" ));");
            stringBuilder.append("\n}\n");
        }

    }
    public String getClassFullName(){
        return mPackageName+"."+mClassName;
    }
    public TypeElement getTypeElement(){
        return typeElement;
    }


}

创建apt-sdk

通过反射调用生成类的方法。

public class BindViewUtil {
    public static void bindView(Activity activity) throws ClassNotFoundException, NoSuchMethodException {
        try {
            Class cla=activity.getClass();
            Class bindViewClass=Class.forName(cla.getName()+"_ViewBinding");
            Method bindViewMethod=bindViewClass.getMethod("bindView",cla);
            bindViewMethod.setAccessible(true);
            bindViewMethod.invoke(bindViewClass.newInstance(),activity);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}
在App中调用

需要加入以下的权限:

   implementation project(":apt-annotation")
   //要用 annotationProcessor ,否则编译不通过
   annotationProcessor project(":apt-procrssor")
   implementation project(':apt-sdk')

使用该Apt

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        try {
            BindViewUtil.bindView(this);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        textView.setText("你好,注解处理器");
    }
}

接下来我们看到生成的类在下面的目录中


image.png
public class MainActivity_ViewBinding{
    public void bindView(com.example.hx.apttest.MainActivity value) { 
        value.textView = (android.widget.TextView)(((android.app.Activity)value).findViewById( 2131165353 ));
}

}

从上面的类中可以看到调用用findViewById的方法。

参考链接:
https://blog.csdn.net/qq_20521573/article/details/82321755
http://77blogs.com/?p=199

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容