实现一个简单的Java编译时注解处理器-阿里云开发者社区

开发者社区> 开发与运维> 正文

实现一个简单的Java编译时注解处理器

简介: ### 简介 Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。 Java语言中的类、方法、变量、参数和包等都可以被标注。Java标注和Javadoc不同,标注有自反性。在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注。 根据元注解`@Retention`指定值的不同,注解可分为`SOURCE`

简介

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。
Java语言中的类、方法、变量、参数和包等都可以被标注。Java标注和Javadoc不同,标注有自反性。在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注。
根据元注解@Retention指定值的不同,注解可分为SOURCECLASSRUNTIME三种类型。当被声明为SOURCE时,注解仅仅在源码级别被保留,编译时被丢弃;声明为CLASS时,注解会由编译器记录在class文件内,但在运行时会被忽略,默认的Retention级别即为CLASS;声明为RUNTIME时,注解将被保留到运行时,可通过反射在运行时获取到。
下面我们针对CLASS级别的注解,介绍在编译期处理注解的方法。

APT

注解处理器(Annotation Processing Tool)是javac内置的工具,用于在编译时期扫描和处理注解信息。从JDK 6开始,apt暴露了可用的API。一个特定的处理器接收一个Java源代码或已编译的字节码作为输入,然后输出一些文件(通常是.java文件)。这就意味着你可以使用apt动态生成代码逻辑,需要注意的是apt仅可以生成新的Java类而不能对已存在的Java类进行修改。所有生成的Java类将和其他源代码一起被javac编译。

定义和使用注解

举个栗子,此处我们定义一个用于标注Field的注解Meta,包含两个参数repeat和id,在编译阶段我们将通过处理这一注解,给被标注的Field赋值,如repeat为2,id为Aa,则被标注的Field会被赋值为"AaAa"。

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Meta {
    int repeat() default 0;
    String id() default "";
}

在Field上使用注解

@Meta(repeat = 3, id = "^_^")
public String test;

处理注解

下面我们基于Android Studio编写一个处理上文中定义的Meta注解的处理器。

创建Module

此处我们将注解解析器作为Android Project中的一个module来开发,新建一个Module,类型选择Java Library。

创建处理器

注解需要通过注解处理器进行处理,所有的注解处理器都实现了Processor接口,一般我们选择继承AbstractProcessor来创建自定义注解处理器。
继承AbstractProcessor,实现public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)方法。方法参数中annotations包含了该处理器声明支持并已经在源码中使用了的注解,roundEnv则包含了注解处理的上下文环境。 此方法返回true时,表示此注解已经被处理完毕,返回false时将会交给其他处理器继续处理。

声明支持的注解类型和源码版本

覆盖getSupportedSourceVersion方法,返回处理器支持的源码版本,一般直接返回SourceVersion.latestSupported()即可。
覆盖getSupportedAnnotationTypes方法,返回处理器想要处理的注解类型,此处需返回一个包含了所有注解完全限定名的集合。
在Java 7及以上,可以使用类注解@SupportedAnnotationTypes@SupportedSourceVersion替代上面的方法进行声明。

声明注解处理器

注解处理器在使用前需要先向JVM注册,在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,在此文件内逐行声明注解处理器。同样地,此处需要声明的也是处理器类的完全限定名。
另一个简便的方法是使用Google提供的auto-services库,在build.gradle中引入com.google.auto.service:auto-service:1.0-rc2,并在处理器类上添加注解@AutoService(Processor.class),auto-services也是一个注解处理器,会在编译时为该module生成声明文件。

解析注解

首选我们定义一个接口来规范生成的类:

public interface Actor {
    void action();
}

再定义一个类结构来描述我们生成的Java类:

public class TargetGen<T extends Target> implements Actor{
    protected T target;

    public TargetGen(T obj) {
        this.target = obj;
    }

    @Override
    public void action() {
        //赋值操作
    }
}

如果我们有一个类A,其中的Field f包含了Meta注解,我们会为其生成一个AGen类,并在action方法中完成对f的赋值操作。
在process方法中完成对注解的解析和代码生成操作:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    /*roundEnv.getRootElements()会返回工程中所有的Class
    在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描*/
    for (Element e : roundEnv.getRootElements()) {
        List<String> statements = new ArrayList<>();
        /*遍历Class内所有元素*/
        for (Element el : e.getEnclosedElements()) {
            /*只处理包含了注解并被修饰为public的Field*/
            if (el.getKind().isField() && el.getAnnotation(Meta.class) != null && el.getModifiers().contains(Modifier.PUBLIC)) {
                /*获取注解信息,生成代码片段*/
                Meta meta = el.getAnnotation(Meta.class);
                int repeat = meta.repeat();
                String seed = meta.id();
                String result = "";
                for (int i = 0; i < repeat; i++) {
                    result += seed;
                }
                statements.add("\t\ttarget." + el.getSimpleName() + " = \"" + result + "\";");
            }
        }
        if (statements.size() == 0) {
            return true;
        }

        String enclosingName;
        if (e instanceof PackageElement) {
            enclosingName = ((PackageElement) e).getQualifiedName().toString();
        } else {
            enclosingName = ((TypeElement) e).getQualifiedName().toString();
        }
        /*获取生成类的类名和package*/
        String pkgName = enclosingName.substring(0, enclosingName.lastIndexOf('.'));
        String clsName = e.getSimpleName() + "Gen";
        log(pkgName + "," + clsName);
        /*创建文件,写入代码内容*/
        try {
            JavaFileObject f = processingEnv.getFiler().createSourceFile(clsName);
            log(f.toUri().toString());
            Writer writer = f.openWriter();
            PrintWriter printWriter = new PrintWriter(writer);
            printWriter.println("//Auto generated code, do not modify it!");
            printWriter.println("package " + pkgName + ";");
            printWriter.println("\nimport com.moxun.Actor;\n");
            printWriter.println("public class " + clsName + "<T extends " + e.getSimpleName() + "> implements Actor{");
            printWriter.println("\tprotected T target;");
            printWriter.println("\n\tpublic " + clsName + "(T obj) {");
            printWriter.println("\t\tthis.target = obj;");
            printWriter.println("\t}\n");
            printWriter.println("\t@Override");
            printWriter.println("\tpublic void action() {");
            for (String statement : statements) {
                printWriter.println(statement);
            }
            printWriter.println("\t}");
            printWriter.println("}");
            printWriter.flush();
            printWriter.close();
            writer.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
    return true;
}

在目标module的dependencies中加入处理器模块的依赖,clean并rebuild工程,源代码就能被自定义的注解处理器处理并将产出的类生成到build/intermediates/classes目录下。由于一个Android Gradle插件的issue,直到插件版本2.2.0-alpha4,产出的class仍会被放到此目录下。intermediates目录下的源文件不会被IDE索引,所以给生成代码的调试带来一些不便,不过这并不影响后续的编译过程。在未来的版本中,该issue可能会被修正,产物会被输出到正确的地方也就是build/generated/source/apt目录下。

在运行时使用生成的类

在运行时可以使用反射来访问生成的类,此处定义了一个简单的帮助类来实例化生成的类并给目标Field赋值:

public class MetaLoader {
    public static void load(Object obj) {
        String fullName = obj.getClass().getCanonicalName();
        String pkgName = fullName.substring(0, fullName.lastIndexOf('.'));
        String clsName = pkgName + "." + obj.getClass().getSimpleName() + "Gen";

        try {
            Class<Actor> clazz = (Class<Actor>) Class.forName(clsName);
            Constructor<Actor> constructor = clazz.getConstructor(obj.getClass());
            Actor actor = constructor.newInstance(obj);
            actor.action();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在目标类初始化的时候调用MetaLoader.load,传入目标类的实例,便可完成对Field的赋值操作。

在打包过程中排除处理器

由于在前面引入了auto-service库,最终打包apk的时候会报错Duplicate files copied in APK META-INF/services/javax.annotation.processing.Processor,而该文件在运行时又是不需要的,所以可以在packagingOptions中排除这个文件以规避该错误:

packagingOptions {
    exclude 'META-INF/services/javax.annotation.processing.Processor'
}

然而这并不是彻底的解决方案,如上所述,注解处理器在运行时是完全无用的,能否让其仅存在于编译期而不打包进最终产物内呢?答案是肯定的。
在工程的build.gradle内添加插件:

dependencies {
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' 
     //etc……
}

在module的build.gradle内应用插件:

apply plugin: 'com.neenbedankt.android-apt'

应用插件后,dependencies会新增一个新的依赖方法apt,修改依赖声明为:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    apt project(':processor')
    //etc……
}

如此声明后处理器module内的类将不会被打包到最终的产物中,有利于缩小产物体积。

调试注解处理器

在Android Studio中添加新的Run/Debug Configurations,类型选择Remote;
在工程的gradle.properties中添加

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

选中上面定义的Configuration,点击Debug按钮等待目标进程attach;
在注解处理器逻辑内设置断点,选择Rebuild Project,触发注解处理器处理逻辑即可实现断点调试。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章