(Android)注解系列-编译时注解

写在前面:

  • 环境:Android Studio 3.0
  • 本文目的:运行时注解一般与反射搭配使用,Android中强烈不建议使用反射,所以一般的注解框架都是采用编译时注解。本文通过一个小例子来认识编译时注解。
  • 需要了解注解的基本概念,gradle基础,以及一些注入的概念。如对注解一无所知可参考这篇:(Android)注解系列-注解基本概念

正文

一、效果需求

假如我们在MainActivity的类上声明两个水果类,并且通过注解的方式进行初始化属性。点击按钮时输出这两个对象的信息

public class MainActivity extends AppCompatActivity {
    /**
     * 在这添加我们需要的注解属性,初始化实例的时候需要读取注解内容,
     */
    @FruitProperties(name = "红富士",price = 5.5)
    Apple apple;

    @FruitProperties(name = "小米蕉",price = 20.0)
    Bananer bananer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //通过依赖的注解模块提供的FruitInject对我们注解的对象进行创建实例并赋值,赋值根据注解的属性
        FruitInject.inject(this);

        initClickListener();
    }
    /**
     * 效果演示
     */
    private void initClickListener() {
        Button button = findViewById(R.id.btn_show);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,apple.toString()+bananer.toString(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}
    /**
     * 效果演示
     */
    private void initClickListener() {
        Button button = findViewById(R.id.btn_show);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,apple.toString()+bananer.toString(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

二、模块分析

项目分成四个模块:

1.Annotation:存放我们定义的所有注解(java lib)
2.Api:注解对外的统一接口(java/Android lib根据需求)
3.AnnotationCompiler:注解处理器模块,在编译时读取注解并生java源码文件。(与app无关,仅生成代码,打包apk时不打包进去。java lib)
4.app:项目模块,仅为了演示编译时注解

如图:


模块.png

使用别人的编译时注解框架会发现一般都是两个依赖包,一个是Api+Annotation模块,一个是compiler模块。

三、实现步骤

  • 3.1 Annnotation 模块

创建Annotation模块,存放所有的注释类,这里仅有一个注释类:

/**
 * 说明:水果属性注解,有name和price两个属性
 *
 * @author LJY on 2017/11/14
 */

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface FruitProperties {
    String name();
    double price();
}
  • 3.2 Api模块

首先我们先写一个FruitProvider,我们的注解处理器会生成一个该接口的实现类,怎么生成后面会介绍:

public interface FruitProvider<T> {
    void provide(T host);
}

然后还需要一个注入类,提供统一的注入入口:

public class FruitInject {

    /**
     * 注入方法,其实内部调用FruitProvider的方法
     * @param host 我们传入的对象(MainActivity),访问其内部属性(Apple,Bananer)需要
     */
    public static void inject(Object host){
        try {
            //获取frutiProvider接口的实现类,并调用接口方法。该实现类是通过注解处理器生成的
            String hostName = host.getClass().getName();
            Class<?> fruitProviderClass = Class.forName(hostName + "$$FruitProvider");
            FruitProvider fruitProvider = (FruitProvider) fruitProviderClass.newInstance();
            fruitProvider.provide(host);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

以上两步就是该模块必须的内容,为了演示我们还添加了一个Fruit接口:

/**
 * 说明:水果接口
 * @author LJY on 2017/11/14
 */
public interface Fruit {
    Fruit init(String name, double price);
}
  • 3.3注解处理器模块(本文的重点,核心)

3.3.1依赖文件:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
    //注册依赖(可以通过autoService注册该注释处理器)
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    //java文件生成的工具类
    implementation 'com.squareup:javapoet:1.9.0'
    //我们需要处理的注解模块
    implementation project(':Annotation')
}

3.3.2注解处理器
所有的注解处理器都需要实现javax.annotation.processing.Processor。这里我们定义MyAnnotationProcessor直接继承AbstractProcessor抽象类。我们会重写他的四个方法

    //返回支持的注解类型,这个方法我们会用注释代替
    Set<String> getSupportedAnnotationTypes();
    //返回支持的源码版本,这个方法我们会用注释代替
    SourceVersion getSupportedSourceVersion();
    //在这里和可以获得一些工具类
    void init(ProcessingEnvironment var1);
    //真正的处理在这里,必须
    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

此外我们还需要注册这个注解处理器,这样编译器才知道我们需要加载这个注解处理器。直接在MyAnnotationProcessor添加@AutoService(Processor.class),也有别的方式注册,这里不介绍。
上代码,注释写的比较详细:

/**
 * 注:输出文件对象生成的java源码文件:MainActivity$$FruitProvider,本例子的类元素:MainActivity
 */
@AutoService(Processor.class)//注册注释处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持源码版本
@SupportedAnnotationTypes("www.ljy.annotation.FruitProperties")//该处理器支持的注解类
public class MyAnnotationProcessor extends AbstractProcessor {
    /**
     * 文件输出工具类
     */
    private Filer mFiler;

    /**
     * 元素辅助工具类
     * 元素解释,一个java文件含有多个元素,包括类元素,方法元素,属性元素等...类似于xml文件
     */
    private Elements mElementUtils;

    /**
     * 这里保存我们要输出的文件信息对象集合
     */
    private Map<String ,JavaFileInfo> mJavaFileInfos =new TreeMap<>();


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

        //此外还有很多工具,如有需求,请自行查阅
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();

    }

    /**
     * 主要的处理在这里
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mJavaFileInfos.clear();
        //获取所有标记了FruitProperties注解的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FruitProperties.class);
        //初始化所有要输出文件信息对象,保存到集合里
        initFileInfos(elements);
        //输出所有的java源码文件
        outputJavaFile();
        return true;
    }

    /**
     * 将所有的文件信息对象输出java源码文件
     */
    private void outputJavaFile() {
        for (JavaFileInfo javaFileInfo: mJavaFileInfos.values()){
            try {
                javaFileInfo.generatedFile().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 遍历所有元素(这里都是属性元素),提取他的类元素,查map是否已经有该类元素对应的输出文件信息对象,不存在则创建并且添加到集合中。
     * 然后将该属性元素添加到输出文件信息对象中。
     * @param elements
     */
    private void initFileInfos(Set<? extends Element> elements) {
        for (Element element:elements){
            //获取元素的最外层(类元素,这里是MainActivity.class)
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String fullName=enclosingElement.getQualifiedName().toString();
            JavaFileInfo javaFileInfo = mJavaFileInfos.get(fullName);
            if (javaFileInfo==null){
                javaFileInfo= new JavaFileInfo(enclosingElement,mElementUtils);
                mJavaFileInfos.put(fullName,javaFileInfo);
            }
            javaFileInfo.addFruitField(new FruitField(element));

        }
    }
}

以上就是核心步骤了
3.3.3其他类介绍:

  • FruitField:封装属性元素信息,包括元素信息,元素注解信息
public class FruitField {

    private VariableElement mVariableElement;
    private String name;
    private double price;
    /**
     * @param element
     */
    FruitField(Element element) {
        //如果这个元素的类型不是《属性元素》抛出异常
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%$", FruitProperties
                    .class.getSimpleName()));
        } else {
            mVariableElement = (VariableElement) element;
            //获取属性元素上的注解信息
            FruitProperties fruitProperties=mVariableElement.getAnnotation(FruitProperties.class);
            name=fruitProperties.name();
            price=fruitProperties.price();

        }
    }
    /**
     * @return 元素名称
     */
    Name getFieldName(){
        return mVariableElement.getSimpleName();
    }

    /**
     * @return 注解Name值
     */
    String getFruitName(){
        return name;
    }

    /**
     * @return 注解price值
     */
    double getPrice(){
        return price;
    }

    /**
     * @return 属性类型
     */
    TypeMirror getFieldType(){
        return  mVariableElement.asType();
    }

}
  • JavaFileInfo:封装输出文件信息,可以生成JavaFile对象来输出源码文件,通过这个对象来配置要生成的JAVA文件的包,类,方法等属性
public class JavaFileInfo {
    /**
     * 该输出文件(MainActivity$$FruitProvider.class)对应的类元素(MainActivity.class)
     */
    private TypeElement mTypeElement;
    /**
     * 输出文件对应类元素的属性变量集合
     */
    private ArrayList<FruitField> mField;

    /**
     * 元素辅助工具类
     */
    private Elements mElementUtils;

    public JavaFileInfo(TypeElement element, Elements elementUtils) {
        mTypeElement = element;
        mElementUtils = elementUtils;
        mField = new ArrayList<>();
    }

    public void addFruitField(FruitField fruitField) {
        mField.add(fruitField);
    }


    /**
     * @return 输出文件对象
     */
    public JavaFile generatedFile() {
        MethodSpec methodSpec = generatedMethod();
        TypeSpec typeSpec = generatedClass(methodSpec);
        String packageName = generatedPackage();
        JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
        return javaFile;
    }

    /**
     * @return 包名
     */
    private String generatedPackage() {
        //通过类元素获取包名
        String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();
        return packageName;
    }

    /**
     * @param methodSpec 构造方法
     * @return 构造类
     */
    private TypeSpec generatedClass(MethodSpec methodSpec) {
        TypeSpec typeSpec = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$FruitProvider")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(getFruitProviderClassName(), TypeName.get(mTypeElement.asType())))
                .addMethod(methodSpec).build();
        return typeSpec;
    }

    private MethodSpec generatedMethod() {
        MethodSpec.Builder provideMethodBuilder = MethodSpec.methodBuilder("provide");
        provideMethodBuilder
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host");
        for (FruitField fruitField : mField) {
            provideMethodBuilder.addStatement("host.$L=new $L().init(\"$L\",$L)", fruitField.getFieldName(), ClassName.get(fruitField.getFieldType()),fruitField.getFieldName(), fruitField.getPrice());
        }

        return provideMethodBuilder.build();
    }

    private ClassName getFruitProviderClassName() {
        return ClassName.get("www.ljy.api", "FruitProvider");
    }
    
}

完成以上步骤并编译下就可以生成我们想要的java代码文件了,这里生成的是MainActivity$$FruitProvider

public class MainActivity$$FruitProvider implements FruitProvider<MainActivity> {
  @Override
  public void provide(MainActivity host) {
    host.apple=new www.ljy.annotationprocessordemo.Apple().init("apple",5.5);
    host.bananer=new www.ljy.annotationprocessordemo.Bananer().init("bananer",20.0);
  }
}

4.app演示模块
依赖:

    implementation project(':annotations')
    implementation project(':api')
    //annotationProcessor依赖的包不会打包进apk中
    annotationProcessor project(':complier')

除了一开始看到的MainActivity外,还有演示用的apple类:

public class Apple implements Fruit {

    String mName;
    double mPrice;
    public Apple() {

    }
    @Override
    public Apple init(String name, double price){
        mName=name;
        mPrice=price;
        return  this;
    }

    @Override
    public String toString() {
        return "Apple{" + "mName='" + mName + '\'' + ", mPrice=" + mPrice + '}';
    }
}
  • 四、总结

其实最终打包进apk的只有我们的annotation+api+app模块,compiler只是我们自己写的一个工具而已,我们用这工具生成我们想要的代码(该代码在app-build-generated-source -apt-debug下)

结语

注1:如果编译时出现 错误: 编码GBK的不可映射字符 请在对应的模块gradle添加:

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

注2:本例仅为了讲一些基础型的知识,不考虑例子中内存泄露等问题。
注3:注解处理器在什么时候运行呢?他的优先级是非常高的,可以理解成编译的第0步。
注4:源码地址: AnnotationProcessorDemo

参考

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

推荐阅读更多精彩内容