美文网首页
(Android)注解系列-编译时注解

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

作者: 李_3d1b | 来源:发表于2017-11-15 14:39 被阅读0次

写在前面:

  • 环境: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

参考

相关文章

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

    写在前面: 环境:Android Studio 3.0 本文目的:运行时注解一般与反射搭配使用,Android中强...

  • Android进阶之自定义注解

    Android进阶之自定义注解 本篇文章内容包括: 注解的概念 元注解 自定义注解 Android自定义编译时注解...

  • java注解

    内容: 注解的定义 注解的语法 源码级别的注解的使用 运行时注解的使用 编译时注解的使用 Android 预置的注...

  • Android编译时注解

    Android编译时注解 [TOC] 前言 相信大家经常都使用到注解,如果使用过AndroidAnnotation...

  • Android 编译时注解 —— 语法详解

    java Type 详解 java 反射机制详解 注解使用入门(一) Android 自定义编译时注解1 - 简单...

  • Android-注解在Android中的应用

    Android 应用开发中对注解的使用达到了淋漓尽致的地步,无论是运行时注解,还是编译时注解,或是标准注解,都被广...

  • 编译时注解器初探(一)

    编译时注解器初探(一) 注解处理器 (Annotation Processor) 编译时注解和运行时注解定义的方式...

  • 平时碰到问题的解决方案

    调试Annotation Processor编译时注解器 Android gradle provided、impl...

  • Android编译时注解

    之前写了注解基础和运行时注解这篇文章,里面使用运行时注解来模仿ButterKnife绑定控件ID的功能,运行时注解...

  • java注解以及自定义运行时注解

    前言:Android中比较常用的框架使用了注解技术ButterKnife,Dagger2通过编译时注解处理技术在编...

网友评论

      本文标题:(Android)注解系列-编译时注解

      本文链接:https://www.haomeiwen.com/subject/wypnmxtx.html