java注解学习(2)-利用注解生成动态代码

前言

在前面我们学习过java的注解的语法,说明了注解的作用;前面举的例子是运行时注解,这样会影响我们代码执行的效率;本章我们看看编译时注解的使用方式,可以利用注解在编译时生成代码,已提高我们的工作效率;其关键就在AbstractProcessor(注解处理器)

示例

废话不多说,我们直接看一个简单的示例:

注解处理器

java library

首先创建一个java library,用来创建我们的注解处理器
Gradle module java-library

apply plugin: 'java-library'



dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api 'com.google.auto.service:auto-service:1.0-rc3'
}

tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

定义一个我们的自定义注解

package com.example.testabstractprocessor;

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

@Retention(RetentionPolicy.SOURCE) //编译时起作用
@Target(ElementType.FIELD) //注解作用在field
public @interface TestBindView {
    int value() default -1;
}

针对上面的自定义注解,编写注解处理器

package com.example.testabstractprocessor;

import com.google.auto.service.AutoService;

import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashSet;
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.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;


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

    private Filer mFiler;//用于生成java文件
    private Messager mMessager; //用于生成日志
    private Elements mElements; //用于解读annation注解对象的相关信息的辅助类

    public void note(String log){
        mMessager.printMessage(Diagnostic.Kind.WARNING,log);
    }

    public void note(String format,Object ...args){
        mMessager.printMessage(Diagnostic.Kind.WARNING,String.format(format,args));
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        /*
        该方法返回一个Set<String>,代表ButterKnifeProcessor要处理的注解类的名称集合,即 ButterKnife 支持的注解:butterknife-annotations
        * */
        //表示,自定义的annotation的名称集合
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(TestBindView.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //返回当前系统支持的 java 版本
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        /*
        首先 init() 方法完成sdk版本的判断以及相关帮助类的初始化,帮助类主要有以下几个:
        Elements elementUtils,注解处理器运行扫描源文件时,以获取元素(Element)相关的信息。Element 有以下几个子类:
        包(PackageElement)、类(TypeElement)、成员变量(VariableElement)、方法(ExecutableElement)
        //总之,是用来表示被标注的信息
        Types typeUtils,
        Filer filer,用来生成 java 类文件。
        Trees trees,
        **/
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElements = processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //完成了目标类信息的收集并生成对应 java 类
        Set<? extends Element> TestBindViewElements = roundEnvironment.getElementsAnnotatedWith(TestBindView.class);
        for(Element element:TestBindViewElements){
            //获取包名
            PackageElement packageElement = mElements.getPackageOf(element);
            String pkgName = packageElement.getQualifiedName().toString();
            note("package name = %s",pkgName);

            //获得包装类类名
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = typeElement.getQualifiedName().toString();
            note("Enclosing class name = %s",enclosingName);

            //TestBindView是针对view进行注解的
            String fieldName = ((VariableElement)element).getSimpleName().toString(); //field的名字
            String fieldClassType = ((VariableElement)element).asType().toString(); //field的类型
            TestBindView testBindView = element.getAnnotation(TestBindView.class);
            int fieldValue = testBindView.value(); //注解的数据
            note("field %s %s = %d",fieldClassType,fieldName,fieldValue);

            //生成辅助文件
            createFile(typeElement,fieldClassType,fieldName,fieldValue);
            return true;

        }
        return false;
    }

    public void createFile(TypeElement enclosingClass,String fieldType,String fieldName,int fieldValue){
        String pkgName = mElements.getPackageOf(enclosingClass).getQualifiedName().toString();
        String enclosingName = enclosingClass.getQualifiedName().toString();
        try {
            JavaFileObject javaFileObject = mFiler.createSourceFile(enclosingName+"_TestBindView"
                                                                   ,new Element[]{}); //要生成的java文件
            Writer writer =  javaFileObject.openWriter();
            writer.write(brewCode(pkgName,fieldType,fieldName,fieldValue));
            writer.flush();
            writer.close(); //创建对应的java文件

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String brewCode(String pkgName,String fieldType,String fieldName,int fieldValue){
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkgName + ";\n\n");
        builder.append("//Auto generated by AbstractProcessor,do not modify!!\n\n");
        builder.append("public class MainActivity_TestBindView { \n\n");
        builder.append("public static void main(String[] args){ \n");
        String info = String.format("%s %s = %d", fieldType, fieldName, fieldValue);
        builder.append("System.out.println(\"" + info + "\");\n");
        builder.append("}\n");
        builder.append("}");
        return builder.toString();

    }
}

注意点1:
@AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
AutoService这里主要是用来生成
META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册,具体手动注册方法如下:
1.创建一个
META-INF/services/javax.annotation.processing.Processor文件,
其内容是一系列的自定义注解处理器完整有效类名集合,以换行切割
2.将自定义注解处理器和
META-INF/services/javax.annotation.processing.Processor打包成一个.jar文件。所以其目录结构大概如下所示:

MyProcessor.jar
    - com
        - example
            - MyProcessor.class

    - META-INF
        - services
            - javax.annotation.processing.Processor

注意点2:
init(ProcessingEnvironment env):每个Annotation Processor必须***
有一个空的构造函数 *。编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnviroment参数,通过该参数可以获取到很多有用的工具类: Elements (用于存储被注解的信息), Types , Filer(用于生成java文件) **等等
process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv):Annotation Processor扫描出的结果会存储进roundEnv中,可以在这里获取到注解内容,编写你的操作逻辑。注意,process()函数中不能直接进行异常抛出,否则的话,运行Annotation Processor的进程会异常崩溃,然后弹出一大堆让人捉摸不清的堆栈调用日志显示.
getSupportedAnnotationTypes(): 该函数用于指定该自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),注解(Annotation)指定必须是完整的包名+类名(eg:com.example.MyAnnotation)
getSupportedSourceVersion():用于指定你的java版本,一般返回:SourceVersion.latestSupported()。当然,你也可以指定具体java版本:
return SourceVersion.RELEASE_7;

经过前面3个步骤后,其实就已经算完成了自定义Annotation Processor。后面要做的就是在源码里面,在需要的地方写上我们自定义的注解就行了。
以上注解处理器就使用完毕,讲生成的jar文件cp到app/libs文件夹下,在app端使用

app

Gradle module app

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    annotationProcessor files('libs/TestAbstractProcessor.jar')
}
package com.example.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import com.example.testabstractprocessor.TestBindView;

public class MainActivity extends AppCompatActivity {

   @TestBindView(R.id.textView)
    TextView tv; //此处对TextView使用了注解

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

参考注解处理器(Annotation Processor)简析
注意点:

  1. 在 app/build/generated/source/apt/debug下面生成了想要的文件
package com.example.myapplication;

//Auto generated by AbstractProcessor,do not modify!!

public class MainActivity_TestBindView { 

public static void main(String[] args){ 
System.out.println("android.widget.TextView tv = 2131165319");
}

2.执行gradlew build
就可以在Android Studio下的terminal中看到
警告: package name = com.example.myapplication
警告: Enclosing class name = com.example.myapplication.MainActivity
警告: field android.widget.TextView tv = 2131165319
通过Messager在编译期间打出log

踩坑

总结下踩过得坑

首先在Android Stuido里面创建一个java library module

https://blog.csdn.net/u013087553/article/details/71747327

使用创建java library

https://www.cnblogs.com/jpfss/p/9875402.html

java生成jar包
META-INF https://www.cnblogs.com/mq0036/p/8566427.html

错误: 编码GBK的不可映射字符
https://www.cnblogs.com/fply/p/8327759.html

module:app

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    api project(path: ':TestAbstractProcessor')
}

这样配置不行
Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration.

Gradle 3.0以后,先将生成的jar包复制过去app project下面的libs文件夹

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    annotationProcessor files('libs/TestAbstractProcessor.jar')
}

拓展延伸

以上其实就是ButterKnife的基本原理;也是利用注解处理器,对自定义的注解生成对应的java文件;具体可参考ButterKnife 原理解析
总结来说就是注解处理器+JavaPoet(更方便的生成java文件)
生成类似于

// Generated code from Butter Knife. Do not modify!
package com.example.test1;

import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import butterknife.Unbinder;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;

public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
  protected T target;

  private View view2131165218;

  @UiThread
  public MainActivity_ViewBinding(final T target, View source) {
    this.target = target;

    View view;
    target.test = Utils.findRequiredViewAsType(source, R.id.test, "field 'test'", TextView.class);
    view = Utils.findRequiredView(source, R.id.button, "field 'btn' and method 'btn_click'");
    target.btn = Utils.castView(view, R.id.button, "field 'btn'", Button.class);
    view2131165218 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.btn_click();
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    T target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");

    target.test = null;
    target.btn = null;

    view2131165218.setOnClickListener(null);
    view2131165218 = null;

    this.target = null;
  }
}

总结

注解一般有运行时处理和编译时处理两种使用方式,这里我们都学习了一遍,可以看出,注解对于一些大型框架,效率工具而言,是一种利器;希望有一天自己能根据工作中的需求用到注解的使用

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

推荐阅读更多精彩内容

  • 这个夏天,酷暑近乎猖狂 似一个无赖,懒着不走 这些日子,夫夷江不再丰满 如一个清秀的姑娘 那些池塘是很容易倒影的 ...
    白云之外阅读 353评论 0 1
  • 版权说明:本篇文章由"猫科龙"翻译,译者已获得原作者Translate及Republish授权。版权所有,未经译者...
    猫科龙阅读 677评论 2 1
  • 多哄哄自己,没什么大不了,都是你想出来的…… 穷也会让我更敏锐 喜欢她的话,饿一点,慢慢把微饱当成一种状态,而不是撑。
    黏玉米阅读 180评论 0 0
  • 十里春风,不如你的播出,不仅吸引了许多的年轻人,也让老一辈的人对年轻时代的回想。里面的情节吸引了大多数的人,...
    傻蛋么么阅读 229评论 0 0
  • 莱斯, 9岁时,圣诞节到了,别家孩子都得到家长赠送的礼 物 , 有跑车,有新衣服,有家电…… 而莱斯的父亲却给...
    旖旎i阅读 288评论 0 2