源码级注解

请看前言


上一篇文章我们说到运行时框架是在虚拟机运行程序时使用反射技术搭建的框架;而源码级框架是在javac编译源码时,生成框架代码或文件。源码级别框架发生过程是在编译期间,并不会过多影响到运行效率。因此,Android等对效率性能要求较高的平台一般使用源码级别注解来搭建。

注解处理器


注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。

一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!
生成的Java代码是在新的.java文件中,所以你并不能修改已存在的Java类,例如向已有的类中添加方法。并且这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

写代码之前


在开始写代码之前,我们需要了解一个叫AbstractProcessor的类。AbstractProcessor(虚处理器),是注解处理器核心API。注解处理器需要继承于AbstractProcessor,如下所示:

public class MyProcessor extends AbstractProcessor {

    // 这个方法主要是获取工具类,有Elements, Types和Filer等。后面会提到
    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    // 相当于main(),写处理的过程
    // annotations是getSupportedAnnotationTypes()的子集
    // env代表这一轮扫描后的结果,返回true则表示消费完此次扫描,此轮扫描注解结束
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    // 在这里定义你的注解处理器注册到哪些注解上
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    // 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()
    @Override
      public SourceVersion getSupportedSourceVersion() { }

 }

Java 7,我们可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()。但是因为兼容原因(特别针对Android平台),建议使用重载getSupportedAnnotationTypes()和getSupportedSourceVersion()的方式。

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

}

值得一提
注解处理器是运行它自己的虚拟机JVM中。javac启动一个完整Java虚拟机来运行注解处理器。这意味着我们可以使用任何你在其他java应用中使用的的东西。可以使用依赖注入工具(Dagger等),或者其他想要的类库。

自定义注解处理器


在自定义前,问一个问题,怎么注册Processor到javac中?
我们需要编译产生一个类似这样的.jar文件。(注解处理器代码实现+可扩展应用程序)

MyProcessor.jar
    - com
        - example
            - MyProcessor.class

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

打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是注解处理器的合法的全名列表。(不明白可搜索“可扩展应用程序”)

com.example.MyProcessor

值得为人称道的是,Google老大哥提供了auto-service.jar,极大简化了我们的打包操作。我们可以用它生成META-INF/services/javax.annotation.processing.Processor文件。是的,我们可以在注解处理器中使用注解,只需要一个@AutoService。后面会使用到。

不说废话


场景:我们要帮一个动物学家的朋友完成观察动物的报告。于是我们写了一个Animal接口,然后实现了Bird,Dog,Fish。因为目前只观察到了这几种。学过设计模式的朋友会把它们抽取出来,做一个工厂类,这种思路是很好的。但是,假设我们只会简单工厂模式,就if..else..那种。那这时候我们添加一个动物,就需要在工厂自动生成对应的if语句来生成相应实例。

于是,我们想到说使用注解处理器的方法来在编译时自动生成有关代码。

我们可以自定义注解如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Factory {

    // Animal接口实现类
    Class<?> type();

    String id();

}

有了工厂注解后,我们先不急着放在Animal接口实现类上面。我们先实现注解处理器。

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

}

前面我们提到过@AutoService注解,加了此注解,可以自动生成services下的文件。因此,我们不需要手动编写service,只需要关注注解处理器代码逻辑即可。

我们需要一些工具类,在代码中添加并在init()方法对其进行初始化。

private Filer filer;
private Messager messager;

private Types typeUtils;
private Elements elementUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    typeUtils = processingEnv.getTypeUtils();
    elementUtils = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
    messager = processingEnv.getMessager();
}

接着,重载getSupportedAnnotationTypes(),getSupportedSourceVersion()获取注解支持,以及源码编译版本支持。

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotataions = new LinkedHashSet<String>();
    annotataions.add(Factory.class.getCanonicalName());
    return annotataions;
}

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

Elements和TypeMirrors


需要你对工具类的用途大概有个印象。

工具类 用途
Elements 用来处理Element的工具类
Types 用来处理TypeMirror的工具类
Filer 使用Filer你可以创建文件

需要注意
在注解处理的过程中,我们扫描所有的Java源文件。源代码的每一个部分都是一个特定类型的Element。它只是结构化的文本,不是可运行的,可以像你认识Xml一样的方式认识Element。例如说:

package com.example;    // PackageElement

public class Foo {        // TypeElement

    private int a;      // VariableElement
    private Foo other;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                 int newA   // TypeElement
                 ) {}
}

因此,我们可以像Xml那样定位到某个元素。
举例来说,假如你有一个代表public class Foo类的TypeElement元素,你可以遍历它的孩子,如下:

TypeElement fooClass = ... ;  
for (Element e : fooClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == fooClass
}

可以从上面看出来,Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。类信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror

搜索@Factory注解
开始处理process()的逻辑,在其中添加这样一段:

for (Element annotatedElement:roundEnv.getElementsAnnotatedWith(Factory.class))

目的是遍历所有被注解了@Factory的元素,而我们知道TypeElement可能是class,也可能是其他,那么我们在循环中需要加以判断:

if (annotatedElement.getKind() != ElementKind.CLASS) {
    throw new Exception(
            "Only classes can be annotated with @%s");
}
// 确认为ElementKind.CLASS,强制转换为TypeElement
TypeElement typeElement = (TypeElement) annotatedElement;

为什么我们使用.getKind()而不是.instanceof()?
首先,instanceof是检查对象和类的关系的方法。其次,接口(interface)类型也是TypeElement。所以不采用.instanceof()的做法。

优雅的错误处理
可能上面的代码给你造成一种错觉,就是我直接跑出了异常。事实上不是的,上面的代码我写在try..catch..块中,并且使用

messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);

来处理异常。这是一种优雅的错误处理方式。

为什么说是一种优雅的错误处理方式?
在传统Java应用中我们可能就抛出一个Exception。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样)。而使用我们注解处理器的第三方开发者将会从javac中得到非常难懂的出错信息,因为它包含自定义注解处理器的堆栈跟踪(Stacktace)信息。因此,注解处理器就有一个Messager类,它能够打印非常优美的错误信息。除此之外,你还可以链接到出错的元素。在像IntelliJ这种现代的IDE(集成开发环境)中,第三方开发者可以直接点击错误信息,IDE将会直接跳转到第三方开发者项目的出错的源文件的相应的行。当然,这里需要之前初始化好的Messager对象。

接下来,我们会希望有一个类能解析typeElement并保存信息。

public class FactoryAnnotatedClass {
    private TypeElement annotatedClassElement;
    private String qualifiedSuperClassName;
    private String simpleTypeName;
    private String id;

    public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
        this.annotatedClassElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        id = annotation.id();

        if (id == null || "".equals(id)) {
            throw new IllegalArgumentException(
                    String.format(
                            "id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), classElement
                                .getQualifiedName().toString()));
        }

        try {
            // 这个类已经被编译
            Class<?> clazz = annotation.type();
            qualifiedSuperClassName = clazz.getCanonicalName();
            simpleTypeName = clazz.getSimpleName();
        } catch (MirroredTypeException mte) {
            // 这个类还没被编译
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
            simpleTypeName = classTypeElement.getSimpleName().toString();
        }
    }

    /**
     * 获取在{@link Factory#id()}中指定的id return the id
     */
    public String getId() {
        return id;
    }

    /**
     * 获取在{@link Factory#type()}指定的类型合法全名
     *
     * @return qualified name
     */
    public String getQualifiedFactoryGroupName() {
        return qualifiedSuperClassName;
    }

    /**
     * 获取在{@link Factory#type()}{@link Factory#type()}指定的类型的简单名字
     *
     * @return qualified name
     */
    public String getSimpleFactoryGroupName() {
        return simpleTypeName;
    }

    /**
     * 获取被@Factory注解的原始元素
     */
    public TypeElement getTypeElement() {
        return annotatedClassElement;
    }
}

解析并保存信息,接着需要有一个生成工厂类的类。

public class FactoryGroupedClasses {
    /**
     * 将被添加到生成的工厂类的名字中
     */
    private static final String SUFFIX = "Factory";

    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert) throws Exception {

        FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
        if (existing != null) {
            throw new Exception(existing + " is existed.");
        }

        itemsMap.put(toInsert.getId(), toInsert);
    }

    public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
        String factoryClassName = superClassName.getSimpleName() + SUFFIX;

        // 通过filer生成.java文件
        JavaFileObject jfo = filer.createSourceFile(qualifiedClassName + SUFFIX);
        Writer writer = jfo.openWriter();
        JavaWriter jw = new JavaWriter(writer);

        // 写包名
        PackageElement pkg = elementUtils.getPackageOf(superClassName);
        if (!pkg.isUnnamed()) {
            jw.emitPackage(pkg.getQualifiedName().toString());
            // 空一行
        jw.emitEmptyLine();
        } else {
            jw.emitPackage("");
        }

        // public class factoryClassName
        jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
        jw.emitEmptyLine();
    
        // public qualifiedClassName create(String id)
        jw.beginMethod(qualifiedClassName, "create",EnumSet.of(Modifier.PUBLIC), "String", "id");

        jw.beginControlFlow("if (id == null)");
        jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
        jw.endControlFlow();

        for (FactoryAnnotatedClass item : itemsMap.values()) {
            jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
            jw.emitStatement("return new %s()", item.getTypeElement().getQualifiedName().toString());
            jw.endControlFlow();
            jw.emitEmptyLine();
        }

        jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
        jw.endMethod();
        jw.endType();
        jw.close();
    }
}

基本的思路整理清晰后,我们回到process()方法的循环中。

FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement);
...
// 从map集合取数据
FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
if (factoryClass == null) {
    String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
    //初始化生成工厂类的类
    factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
    factoryClasses.put(qualifiedGroupName, factoryClass);
}

在循环结束后,我们这么调用

// Generate code
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
    // 生成工厂类
    factoryClass.generateCode(elementUtils, filer);
}
// 打个标记  这里要注意
factoryClasses.clear();

官方javadoc定义处理过程如下:
注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。
就是说,processor会在执行完process()后,生成工厂类,然后再执行process()以检查生成的文件是否包含@Factory标记,第三次执行发现为none,返回。因此,上面的逻辑是process()会被调用三次。
因此,如果不对数据进行clear,会重复生成第一次生成的文件并且报错。

另外,需要注意的是上面提到一个叫做JavaWriter的类,这是apache的工具类。只不过在工程中我把它复制出来了。很多处理器、库、工具都依赖于JavaWriter。目前已经被JavaPoet取代。有空可以看看。

使用自定义好的注解处理器


使用Maven编译。把annotation.jar和processor.jar放到builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器。

annotation_eclipse.png

为什么要分别打包?
在开发过程中,第三方开放着仅仅需要processor产生需要的代码,而并不希望它跟随源代码一起打包。因此,一起打包不合适!
如果你是一个Android的开发者,你肯定听说过65k个方法的限制(即在一个.dex文件中,只能寻址65000个方法)。如果你在FactoryProcessor中使用guava,并且把注解和处理器打包在一个包中,这样的话,Android APK安装包中不只是包含FactoryProcessor的代码,而也包含了整个guava的代码。Guava有大约20000个方法。所以分开注解和处理器是非常有意义的。

补充Animal的实现类:

@Factory(id = "bird", type = Animal.class)
public class Bird implements Animal {

    @Override
    public void doSomething() {
        System.out.println("fly");
    }
}

写main()方法入口:

public static void main(String[] args) {
    new AnimalFactory().create("dog").doSomething();
}

打开AnimalFactory,

annotation_factory.png

生成的代码符合我们的预期要求。成功!

说说题外话


如果你是一个Android的开发者,你应该非常熟悉一个叫做ButterKnife的注解处理器。在ButterKnife中,你使用@InjectView注解Android的View。ButterKnifeProcessor生成一个MyActivity$$ViewInjector,但是在ButterKnife你不需要手动调用new MyActivity$$ViewInjector()实例化一个ButterKnife注入的对象,而是使用Butterknife.inject(activity)。ButterKnife内部使用反射机制来实例化MyActivity$$ViewInjector()对象:

try {  
    Class<?> injector = Class.forName(clsName + "$$ViewInjector");
} catch (ClassNotFoundException e) { 
    ...
}

上一篇文章我们提到反射影响性能,使用注解处理来生成本地代码,会不会导致很多的反射性能的问题?
的确,反射机制的性能确实是一个问题。然而它并不需要手动去创建对象,确实提高了开发者的开发速度。ButterKnife中有一个哈希表HashMap来缓存实例化过的对象。所以MyActivity$$ViewInjector只是使用反射机制实例化一次,第二次需要MyActivity$$ViewInjector的时候,就直接冲哈希表中获得。
FragmentArgs非常类似于ButterKnife。它使用反射机制来创建对象,而不需要开发者手动来做这些。FragmentArgs在处理注解的时候生成一个特别的查找表类(其实就是一种哈希表),所以整个FragmentArgs库只是在第一次使用的时候,执行一次反射调用(一旦整个Class.forName()的Fragemnt的参数对象被创建),后面的都是本地代码运行了。
而如果你使用过Realm的话,你也能发现类似的细节。

总结


注解处理器是一个强大的工具,为第三方开发者提供了巨大的便捷性。我也想提醒的是,注解处理器可以做到比我上面提到例子复杂很多的事情。
另外,如果你决定在其他类使用ElementUtils, TypeUtils和Messager,你就必须把他们作为参数传进去。可以使用Dagger(一个依赖注入库)来解决这个问题。这在上面也有提到。
对了,我也找到一个使用gradle构建注解处理器的例子。需要的朋友也可以参考一下。地址如下:
http://blog.csdn.net/ucxiii/article/details/52025005

那么,关于注解的模块到此就讲完了。如果有疑问的小伙伴可以在评论区下面留言。

(以上内容参考总结自很多文章,感谢互联网给了我一个学习的平台~)

项目地址:https://github.com/walidake/Annotation_Processor

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

推荐阅读更多精彩内容

  • 什么是注解注解分类注解作用分类 元注解 Java内置注解 自定义注解自定义注解实现及使用编译时注解注解处理器注解处...
    Mr槑阅读 1,063评论 0 3
  • Java 中的注解(Annotation) 是一个很方便的特性在Spring当中得到了大量的应用 , 我们也可以开...
    _秋天阅读 8,635评论 3 22
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,559评论 18 139
  • 我没有辜负自己的期望,在我18岁这一年,我成为了我一直以来希望成为的人类品种之一---渣男。 我很擅长给自己找...
    第九天堂阅读 208评论 0 0
  • 你只知道公众号可以改名了,却不知道如何给公众号起个好名字。 今天打开公众号后台,无意中发现公众号昵称可以修改了,各...
    华一说阅读 6,893评论 4 21