实现一个简单的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,触发注解处理器处理逻辑即可实现断点调试。

目录
相关文章
|
1月前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
88 43
Java学习十六—掌握注解:让编程更简单
|
26天前
|
Java 开发者 Spring
[Java]自定义注解
本文介绍了Java中的四个元注解(@Target、@Retention、@Documented、@Inherited)及其使用方法,并详细讲解了自定义注解的定义和使用细节。文章还提到了Spring框架中的@AliasFor注解,通过示例帮助读者更好地理解和应用这些注解。文中强调了注解的生命周期、继承性和文档化特性,适合初学者和进阶开发者参考。
47 14
|
26天前
|
前端开发 Java
[Java]讲解@CallerSensitive注解
本文介绍了 `@CallerSensitive` 注解及其作用,通过 `Reflection.getCallerClass()` 方法返回调用方的 Class 对象。文章还详细解释了如何通过配置 VM Options 使自定义类被启动类加载器加载,以识别该注解。涉及的 VM Options 包括 `-Xbootclasspath`、`-Xbootclasspath/a` 和 `-Xbootclasspath/p`。最后,推荐了几篇关于 ClassLoader 的详细文章,供读者进一步学习。
32 12
|
1月前
|
分布式计算 大数据 Java
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
25 1
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
|
1月前
|
IDE Java 编译器
Java:如何确定编译和运行时类路径是否一致
类路径(Classpath)是JVM用于查找类文件的路径列表,对编译和运行Java程序至关重要。编译时通过`javac -classpath`指定,运行时通过`java -classpath`指定。IDE如Eclipse和IntelliJ IDEA也提供界面管理类路径。确保编译和运行时类路径一致,特别是外部库和项目内部类的路径设置。
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
45 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
19天前
|
Java 编译器
Java进阶之标准注解
Java进阶之标准注解
29 0
|
1月前
|
IDE Java 编译器
java的反射与注解
java的反射与注解
16 0
|
6天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
5天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。