• 字节码进阶之JSR269详解


    字节码进阶之JSR269详解


    在这里插入图片描述

    前言

    在Java的世界中,我们经常会听到JSR(Java Specification Requests)的名字。JSR是Java社区的一种提案,它定义了Java平台的各种标准和规范。其中,JSR269(Pluggable Annotation Processing API)是Java 6引入的一项关于注解处理的规范。

    注解在Java中是一种常见的元数据形式,它可以提供编译时或运行时的额外信息。通过JSR269,开发者可以创建处理器来处理这些注解,并可在编译时生成额外的源代码或做其他处理。理解和使用JSR269,可以帮助我们更好地利用注解的能力,提高代码的可读性和可维护性。

    JSR-269 代表Java规范请求,而269是这个请求的编号。JSR-269规定了Java编程语言的注解处理API,包括一套用于处理注解的框架和API。

    在这个API中,有一个重要的接口叫做Processor,我们可以实现这个接口来处理特定的注解。Processor接口的实现类将在编译期被调用,并且可以修改源代码、生成新的类等。

    JSR-269分为两个主要的部分:

    1. 声明的API(Declaration API):这部分的API可以用来表示源代码中的程序元素,例如包、类、方法等。

    2. 注解处理API:这部分的API可以让我们定义和使用注解处理器。

    JSR269概览

    JSR269定义了一套Pluggable Annotation Processing API,它允许开发者在编译时处理注解。JSR269主要包括两个部分:

    1. javax.annotation.processing包,它定义了处理注解的框架。例如,Processor接口定义了注解处理器的接口,RoundEnvironment类提供了当前处理轮次的环境信息。

    2. javax.lang.model包,它提供了Java编程元素的模型,使得开发者可以在编译时处理、检查和操作这些元素。

    JSR-269的主要应用场景是在编译期处理注解,例如可以用来生成代码、生成文档、做代码检查等。

    具体的流程如下:

    1. 编译器在扫描源文件的时候,会找到所有的注解,并且根据这些注解找到相应的注解处理器。

    2. 编译器调用注解处理器的process方法,传入注解和元素的信息。

    3. 注解处理器在process方法中处理注解,例如生成新的类、修改源代码等。

    4. 编译器将注解处理器生成的源代码和原来的源代码一起编译。

    举个例子,Lombok项目就广泛使用了JSR-269规范,通过注解处理器在编译期自动生成getter/setter、equals/hashCode等方法,从而减少了手写这些方法的冗余工作。

    深入理解JSR269

    1. 声明的API(Declaration API)

    举个栗子

    以下是一个简单的 JSR 269 例子,我们将创建一个注解处理器,此处理器将验证所有类名称是否符合某种命名规则,例如我们要求所有类名都必须以"Anno"结尾。
    我们可以在任何类上使用 @Anno 注解,编译时,如果类名不符合规则,就会收到一个错误信息。

    创建一个简单的注解

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.TYPE)
    public @interface Anno {
    }
    
    • 1
    • 2
    • 3
    • 4

    创建一个注解处理器:

    @SupportedAnnotationTypes("com.example.Anno")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class AnnoProcessor extends AbstractProcessor {
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            for (Element element : roundEnv.getElementsAnnotatedWith(Anno.class)) {
                if (element.getKind() == ElementKind.CLASS) {
                    TypeElement typeElement = (TypeElement) element;
                    if (!typeElement.getSimpleName().toString().endsWith("Anno")) {
                        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                                "类名必须以'Anno'结尾。", typeElement);
                    }
                }
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这个注解处理器中,首先指定了这个处理器支持处理哪些注解以及支持的Java版本。然后在 process 方法中,获取到所有被 Anno 注解标记的元素,然后检查这些元素是否是类,如果是类并且类名不以 ‘Anno’ 结尾,我打印一条错误信息。

    2. 注解处理API:

    JSR 269 提供了一组用于在编译时扫描和处理注解的 API。这些 API 放置在 javax.annotation.processingjavax.lang.model 包中。

    1. Processor:这是一个接口,您需要实现这个接口以创建自己的注解处理器。

    2. AbstractProcessor:这是一个实现了 Processor 接口的抽象类,为编写注解处理器提供了便利。一般我们直接继承这个抽象类。

    3. RoundEnvironment:提供了注解处理过程的上下文信息。例如,可以通过它来查找包含某个注解的所有元素。

    4. ProcessingEnvironment:提供了处理注解过程中需要的工具。例如,可以通过它来获取元素工具(用于操作元素的工具)和类型工具(用于操作类型的工具)。

    5. Messager:通过 ProcessingEnvironment 获得,用来报告错误消息、警告和其他通知。

    6. Element:表示程序的元素,例如包、类或方法。

    7. TypeElement:表示类和接口元素。

    8. AnnotationValue:表示注解的值。

    9. AnnotationMirror:表示注解。它是一个镜像,因为注解可能并未在运行时保留。

    在创建注解处理器时,需要处理 Processor 接口中的 process 方法,它接收两个参数,一个是注解的集合,另一个是环境上下文。可以通过扫描、访问和处理这些注解来达到我们的目的。

    例如,可以生成额外的源文件或辅助代码,或者验证代码是否符合某些约束。

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Anno.class)) {
            // process the element
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:process 方法返回 true 表示这些注解已经被这个处理器处理,后续的处理器不会再处理它们;返回 false 表示这些注解还未被处理完,可能会有其他处理器处理它们。

    举个栗子

    在举例之前,先明确我们的目标。我们将创建一个注解处理器,该处理器针对我们自定义的注解。当此注解添加到一个类上时,我们的注解处理器将生成一个新的类文件。

    首先,定义一个简单的注解。我们可以在 src/main/java/com/example 目录下创建一个名为 BuilderProperty 的文件,文件内容如下:

    // src/main/java/com/example/BuilderProperty.java
    package com.example;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface BuilderProperty {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后,我们需要实现我们的注解处理器。在 src/main/java/com/example 目录下创建一个名为 BuilderProcessor 的文件,文件内容如下:

    // src/main/java/com/example/BuilderProcessor.java
    package com.example;
    
    import javax.annotation.processing.*;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.*;
    import javax.tools.JavaFileObject;
    import java.io.IOException;
    import java.io.Writer;
    import java.util.Set;
    
    @SupportedAnnotationTypes("com.example.BuilderProperty")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class BuilderProcessor extends AbstractProcessor {
    
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(BuilderProperty.class)) {
          if (element.getKind() == ElementKind.METHOD) {
            MethodElement methodElement = (MethodElement) element;
            TypeElement classElement = (TypeElement) methodElement.getEnclosingElement();
    
            String className = classElement.getQualifiedName().toString();
            String methodName = methodElement.getSimpleName().toString();
    
            try {
              JavaFileObject jfo = processingEnv.getFiler().createSourceFile(className + "Builder");
              Writer writer = jfo.openWriter();
    
              // Write the auto-generated class to the file
              writer.write("public class " + className + "Builder {\n");
              writer.write("  private " + className + " object = new " + className + "();\n");
              writer.write("  public " + className + "Builder " + methodName + "(" + methodElement.getReturnType() + " value) {\n");
              writer.write("    object." + methodName + "(value);\n");
              writer.write("    return this;\n");
              writer.write("  }\n");
              writer.write("  public " + className + " build() {\n");
              writer.write("    return object;\n");
              writer.write("  }\n");
              writer.write("}\n");
    
              writer.close();
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
    
        return true;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    以上,我们的注解处理器就完成了。当编译项目时,上面的 BuilderProcessor 将处理所有的 @BuilderProperty 注解,并为每一个注解生成一个对应的 Builder 类。

    JSR269的应用

    JSR 269 在许多知名的开源框架中都找得到应用

    1. Lombok:Lombok 是一个可以通过注解的方式,让你的 Java 代码更具有简洁性的库。它使用 JSR 269 在编译时生成 getter、setter、equals、hashCode 和 toString 方法,从而减少模板式代码的编写。

    2. Dagger 2:Dagger 2 是一个用于实现依赖注入的框架。它使用 JSR 269 在编译时生成和注入依赖关系的代码,以提高运行时性能。

    3. AutoValue:AutoValue 是 Google 提供的一个生成简洁的、不可修改的自动值类的工具,它使用 JSR 269 在编译时生成这些类的代码。

    4. MapStruct:MapStruct 是一个代码生成库,它基于约定优于配置的方法,利用 JSR 269 在编译时生成对象之间转换的映射代码,从而提高性能。

    5. Immutables:Immutables 是一个生成不可变对象和 builders 的库,它使用 JSR 269 在编译时生成这些代码。

    这些都是 JSR 269 的典型应用,它们通过在编译时生成代码,以提高代码执行的效率和减少运行时的负载。

    1. Dagger:

    首先需要定义一个接口,使用 @Component 注解来告诉 Dagger 如何创建对象。

    @Component
    public interface AppComponent {
        Server server();
    }
    
    • 1
    • 2
    • 3
    • 4

    然后在需要注入的类中定义一个方法,使用 @Inject 注解来告诉 Dagger 需要注入的对象。

    public class Server {
        @Inject
        public Server() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最后建立对象,Dagger 会帮助你创建和注入:

    AppComponent component = DaggerAppComponent.create();
    Server server = component.server();
    
    • 1
    • 2
    1. AutoValue:

    首先定义一个抽象类,使用 @AutoValue 注解告诉 AutoValue 如何创建对象。

    @AutoValue
    abstract class Animal {
        static Animal create(String name, int numberOfLegs) {
            return new AutoValue_Animal(name, numberOfLegs);
        }
        
        abstract String name();
        abstract int numberOfLegs();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后可以创建对象并使用:

    Animal dog = Animal.create("dog", 4);
    String dogName = dog.name(); // Outputs "dog"
    int dogLegs = dog.numberOfLegs(); // Outputs 4
    
    • 1
    • 2
    • 3
    1. MapStruct:

    首先定义一个接口,使用 @Mapper 注解,让 MapStruct 知道如何映射对象。

    @Mapper
    public interface CarMapper {
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        CarDto carToCarDto(Car car);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后可以很容易地转换对象:

    Car car = new Car("Morris", 5, CarType.SEDAN);
    CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
    
    • 1
    • 2
    1. Immutables:

    首先定义一个抽象类,使用 @Value.Immutable 注解告诉 Immutables 如何创建对象。

    @Value.Immutable
    public abstract class ValueObject {
        public abstract String name();
        public abstract int value();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后可以创建对象并使用:

    ValueObject valueObject = ImmutableValueObject.builder()
        .name("name")
        .value(123)
        .build();
    String name = valueObject.name(); // Outputs "name"
    int value = valueObject.value(); // Outputs 123
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意事项和最佳实践

    在使用JSR269时,有一些注意事项和最佳实践。

    1. JSR269只能在编译时处理注解,无法处理运行时的注解。如果你需要处理运行时的注解,应该使用Java Reflection API。

    2. 使用JSR269时应该尽量避免改变原有代码的结构和逻辑,以防引入bug。

    3. 使用JSR269可以提高代码的可读性和可维护性,但是过度使用可能导致代码变得复杂和难以理解。应该在适合的地方适度地使用。

    参考文档

    1. Java Platform, Standard Edition 6 API Specification

    2. JSR 269: Pluggable Annotation Processing API

    3. Processing Annotations with APT

    4. Google Auto:Google Auto是一个使用JSR269来自动生成代码的开源项目,可以作为学习和参考的例子。

    5. Lombok:Lombok项目使用JSR269来自动生成getter和setter方法,是一个很好的实战例子。

    6. Annotation Processing in a Nutshell

  • 相关阅读:
    【EI会议征稿】第八届先进能源科学与自动化国际研讨会(AESA 2024)
    类型体系与基本数据类型(第二节)
    UDP协议
    Python快速刷题网站——牛客网 数据分析篇(十)
    虹科分享 | IOTA网络性能监控 | 如何有效分析VoIP问题
    【mysql体系结构】--mysql的配置文件
    小工具之视频抽帧
    使用xmlrpc连接操作odoo
    基于FPGA开发板使用Verilog设计PWM呼吸灯实验
    【AHK】 MacOS复制粘贴习惯/MacOS转win键位使用习惯修改建议
  • 原文地址:https://blog.csdn.net/wangshuai6707/article/details/133849320