Java核心:注解处理器

Java提供了一个javac -processor命令支持处理标注有特定注解的类,来生成新的源文件,并对新生成的源文件重复执行。执行的命令大概是这样的:

javac -XprintRounds -processor com.keyniu.anno.processor.ToStringProcessor com.keyniu.anno.processor.Point

本文的目标是用一个案例来讲解注解处理器的使用,我们会定义一个@ToString注解,创建注解处理器,为所有标注了@ToString注解的类生成toString工具方法。

这里需要特别说明的是javac -processor只支持生成新的文件,无法在原来的文件里做修改。

1. 定义ToString注解

首先我们需要定义一个注解,用来标注后续要生成toString方法的类。@ToString的逻辑很简单,这里我们只把它定义为一个标记注解。

package com.keyniu.anno.processor;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToString {
}

定义@ToString注解之后,我们就可以把它用在想要自动生成toString方法的类上,比如我们有一个Point类的定义,我们希望为Point类生成toString方法,可以在Point上添加@ToString注解

package com.keyniu.anno.processor;

@ToString
public class Point {
    private int x;
    private int y;

    public int getX(Point this) {
        return x;
    }

    public int getY() {
        return y;
    }
}

2. 创建注解处理器

要想生成代码,我们还需要定义注解处理器,来处理代码的生成。注解处理器需要继承AbstractProcessor类,通过注解能指定支持的注解、代码版本号。下面的代码展示了整个处理过程,我们来解释一下运行流程:

  1. 入参annotations是当前注解处理器支持的注解类型,@SupportedAnnotationTypes可以指定通配符,所以annotaions可以有多个注解类,不过这个案例中,注解只有@ToString
  2. 通过RoundEnvironment.getElementsAnnotatedWith查找标注了@ToString的Element,它有3个子类,TypeElement(类、接口)、VariableElement(字段、参数)、ExecutableElement(方法、构造器)
  3. 这里我们只关心的类类型(TypeElement)
  4. 使用processingEnv.getFiler().createSourceFile创建生成的类文件,此处我们要生成的是com.keyniu.anno.processor.StringUtils类
  5. 创建文件输出流PrintWriter,用于后续写入.java文件
  6. 后续要做的就是通过字符串拼接,生成.java文件的内容了,先定义包,设置import,然后定义类,最后是定义方法的代码,这个过程中可以使用TypeElement的元数据
  7. PrintWriter关闭后,新的.java文件就会倍生成,新生成的类,会重新走一边注解处理的过程
package com.keyniu.anno.processor;

import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;

@SupportedAnnotationTypes({"com.keyniu.anno.processor.ToString"})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ToStringProcessor extends AbstractProcessor {
    public ToStringProcessor() {
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        try {
            Set<? extends Element> es = roundEnv.getElementsAnnotatedWith(ToString.class);  // 步骤2,寻找标准了@ToString的所有Element
            Iterator var4 = es.iterator();

            while(var4.hasNext()) {
                Element e = (Element)var4.next();
                if (e instanceof TypeElement te) { // 步骤3,我们只关心注解了@ToString的TypeElement
                    JavaFileObject jfo = this.processingEnv.getFiler().createSourceFile("com.keyniu.anno.processor.StringUtils", new Element[0]); // 步骤4
                    PrintWriter out = new PrintWriter(jfo.openWriter()); // 步骤5

                    try {
                        this.printClass(out);  // 步骤6
                        this.printMethod(te, out);
                        this.printClassSuffix(out);
                    } catch (Throwable var12) {
                        try {
                            out.close();
                        } catch (Throwable var11) {
                            var12.addSuppressed(var11);
                        }

                        throw var12;
                    }

                    out.close();
                }
            }

            return false;
        } catch (Exception var13) {
            var13.printStackTrace();
            return false;
        }
    }

    private void printClass(PrintWriter out) {
        out.println("package com.keyniu.anno.processor;");
        out.println("");
        out.println("import java.lang.StringBuilder;");
        out.println("");
        out.println("public class StringUtils {");
    }

    private void printClassSuffix(PrintWriter out) {
        out.println("}");
    }

    private void printMethod(TypeElement te, PrintWriter out) {
        String indent = "    ";
        StringBuilder methodCode = new StringBuilder();
        methodCode.append(indent + "public static java.lang.String toString(" + te.getQualifiedName() + " i) {");
        methodCode.append("\n");
        methodCode.append(indent + indent + "StringBuilder sb = new StringBuilder();");
        methodCode.append("\n");
        Iterator var5 = te.getEnclosedElements().iterator();

        while(var5.hasNext()) {
            Element e = (Element)var5.next();
            if (e instanceof VariableElement ve) {
                String field = ve.getSimpleName().toString();
                methodCode.append(indent + indent + "sb.append(\"" + field + ":\");").append("\n");
                methodCode.append(indent + indent + "sb.append(i.get" + field.substring(0, 1).toUpperCase() + field.substring(1) + "());").append("\n");
            }
        }

        methodCode.append(indent + indent + "return sb.toString();\n");
        methodCode.append(indent + "}");
        out.println(methodCode);
    }
}

3. 调用注解处理器

在注解类(ToString)、注解处理器(ToStringProcessor)和使用注解的类(Point)都定义完成后我们就可以开始调用javac -processor类。首先要做的是编译ToString和ToStringProcessor类

javac com/keyniu/anno/processor/ToString.java
javac com/keyniu/anno/processor/ToStringProcessor.java

然后就可以使用-processor引用ToStringProcessor类了,当然你要保证ToStringProcessor类在classpath下可访问

D:\Workspace\HelloJava17\src\main\java>javac -XprintRounds -processor com.keyniu.anno.processor.ToStringProcessor com.keyniu.anno.processor.Point

执行结束后,你会看到在D:\Workspace\HelloJava17\src\main\java\com\keyniu\anno\processor下新生成了一个StringUtils类,生成的代码如下

package com.keyniu.anno.processor;

import java.lang.StringBuilder;

public class StringUtils {
    public static java.lang.String toString(com.keyniu.anno.processor.Point i) {
        StringBuilder sb = new StringBuilder();
        sb.append("x:");
        sb.append(i.getX());
        sb.append("y:");
        sb.append(i.getY());
        return sb.toString();
    }
}

4. 提供Maven支持

应该承认javac -processor确实能用了,但是为编译过程额外添加了一个步骤,带来了额外的负担,而且生成的代码和我们用户代码混杂在一起了。通过Maven的maven-compiler-plugin插件,能让这个过程自动化,并为生成的代码提供单独的目录。为了让这个过程可行,我们现在将项目拆分为两个,anno-processing提供ToString定义、ToStringProcessor注解处理器定义

<groupId>com.keyniu</groupId>
<artifactId>anno-processing</artifactId>
<version>1.0-SNAPSHOT</version>

在客户端工程,提供Point定义,引用anno-processing的依赖, GAV和依赖定义如下

<groupId>com.randy.graalvm</groupId>
<artifactId>swing</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
    <dependency>
        <groupId>com.keyniu</groupId>
        <artifactId>anno-processing</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

紧接着要做的是在swing项目中,添加maven-compiler-plugin插件,定义生成文件保存的目录(generatedSourcesDirectory),以及注解处理器(annotationProcessor)

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <source>17</source>
                <target>17</target>
                <encoding>UTF-8</encoding>
                <generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory>
                <annotationProcessors>
                    <annotationProcessor>
                        com.keyniu.anno.processor.ToStringProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>
    </plugins>
</build>

在这些配置都完成后,就可以正常的通过mvn package编译打包了,运行后能看到target目录下多了一个generated-sources,并且在classes文件夹下包含了StringUtils编译后的.class文件

事情做到这一步,应该说我们定义的ToStringProcessor和ToString已经能满足特定场景下的时候了,不过它并不支持修改,自能新生成一个类来扩展现有类的能力,仍然显得不那么完美。

下一篇我们会讲解lombok的实现原理,怎么在类加载时使用字节码操作类库动态的修改Class的实现。

A. 参考资料

  1. Java Annotation Processing and Creating a Builder | Baeldung
  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring是一个开源的Java应用框架,它的核心特点如下: 1. 轻量级:Spring框架仅仅依赖于少量的第三方库,因此它非常轻量级,不会占用过多的内存和处理器资源。同时,Spring框架具有良好的灵活性,可以通过配置文件或注解来进行各种定制化操作。 2. 控制反转(IoC):Spring框架通过控制反转(IoC)的方式来管理应用对象之间的依赖关系,从而实现了松耦合的设计。这意味着应用对象不再需要自己去创建和管理它们所依赖的对象,而是由Spring框架来负责这些操作。 3. 面向切面编程(AOP):Spring框架提供了面向切面编程(AOP)的支持,通过AOP可以将应用的业务逻辑和系统级服务(如日志、事务等)进行分离,从而增强了应用的可重用性和可维护性。 4. 容器:Spring框架提供了一个容器(ApplicationContext),它可以管理应用中所有的对象,并且可以通过依赖注入(DI)的方式将这些对象注入到应用中的其他对象中。 5. 数据访问:Spring框架提供了对多种数据访问技术的支持,包括JDBC、ORM框架(如Hibernate、MyBatis等)以及NoSQL数据库(如MongoDB等)。 6. MVC框架:Spring框架提供了一个MVC框架(Spring MVC),它可以帮助开发者快速地构建Web应用程序。Spring MVC框架基于MVC(Model-View-Controller)的设计模式,将应用的业务逻辑和表现层进行了分离。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值