代码编织梦想

插入化注解API简介

插件化注解处理(Pluggable Annotation Processing)APIJSR 269提供一套标准API来处理AnnotationsJSR 175,实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotation等Java语言元素映射为Types和Elements,从而将Java语言的语义映射成为对象,我们可以在javax.lang.model包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。
JSR 269用Annotation Processor在编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理.如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止。每执行一次process()方法被称为一个"round",这样整个Annotation processing过程可以看作是一个round的序列。
JSR 269主要被设计成为针对Tools或者容器的API。这个特性虽然在JavaSE 6已经存在,但是很少人知道它的存在。lombok就是使用这个特性实现编译期的代码插入的。另外,如果没有猜错,像IDEA在编写代码时候的标记语法错误的红色下划线也是通过这个特性实现的。KAPT(Annotation Processing for Kotlin),也就是Kotlin的编译也是通过此特性的。

Pluggable Annotation Processing API的核心是Annotation Processor即注解处理器,一般需要继承抽象类javax.annotation.processing.AbstractProcessor。注意,与运行时注解RetentionPolicy.RUNTIME不同,注解处理器只会处理编译期注解,也就是RetentionPolicy.SOURCE的注解类型,处理的阶段位于Java代码编译期间。

使用步骤

插件化注解处理API的使用步骤大概如下:

  1. 自定义一个Annotation
    Processor,需要继承javax.annotation.processing.AbstractProcessor,并覆写process方法。
  2. 自定义一个注解,注解的元注解需要指定@Retention(RetentionPolicy.SOURCE)。
  3. 需要在声明的自定义Annotation
    Processor中使用javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,“包名.注解类型名称”,否则会不生效)。
  4. 需要在声明的自定义Annotation
    Processor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本。
  5. 可选操作,可以通在声明的自定义Annotation
    Processor中使用javax.annotation.processing.SupportedOptions指定编译参数。

实战例子

基础

下面我们模仿一下测试框架Junit里面的@Test注解,在运行时通过Annotation Processor获取到使用了自定义的@Test注解对应的方法的信息。因为如果想要动态修改一个类或者方法的代码内容,需要使用到字节码修改工具例如ASM等,这些操作过于深入,日后再谈。先定义一个注解:

package club.throwable.processor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 1. @author throwable
 2. @version v1.0
 3. @description
 4. @since 2018/5/27 11:18
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
    
}

定义一个注解处理器:

@SupportedAnnotationTypes(value = {"club.throwable.processor.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Log in AnnotationProcessor.process");
        for (TypeElement typeElement : annotations) {
            System.out.println(typeElement);
        }
        System.out.println(roundEnv);
        return true;
    }
}

编写一个主类:

public class Main {

    public static void main(String[] args) throws Exception{
        System.out.println("success");
        test();
    }

    @Test(value = "method is test")
    public static void test()throws Exception{

    }
}

接着需要指定Processor,如果使用IDEA的话,Compiler->Annotation Processors中的Enable annotation processing必须勾选。然后可以通过下面几种方式指定指定Processor。

1、直接使用编译参数指定,例如:javac -processor
club.throwable.processor.AnnotationProcessor Main.java。

2、通过服务注册指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.AnnotationProcessor。

3、通过Maven的编译插件的配置指定如下:

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <annotationProcessors>
                    <annotationProcessor>
                        club.throwable.processor.AnnotationProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

值得注意的是,以上三点生效的前提是club.throwable.processor.AnnotationProcessor已经被编译过,否则编译的时候就会报错:

[ERROR] Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider club.throwable.processor.AnnotationProcessor not found
解决方法有两种,第一种是提前使用命令或者IDEA右键club.throwable.processor.AnnotationProcessor对它进行编译;第二种是把club.throwable.processor.AnnotationProcessor放到一个独立的Jar包引入。我在这里使用第一种方式解决。

最后,使用Maven命令mvn compile进行编译。输出如下:

Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[club.throwable.processor.Test,club.throwable.processor.Main, club.throwable.processor.AnnotationProcessor, processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]
可见编译期间AnnotationProcessor生效了。

进阶

下面是一个例子直接修改类的代码,为实体类的Setter方法对应的属性生成一个Builder类,也就是原来的类如下:

public class Person {

    private Integer age;
    private String name;

    public Integer getAge() {
        return age;
    }

    @Builder
    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    @Builder
    public void setName(String name) {
        this.name = name;
    }
}
生成的Builder类如下:

public class PersonBuilder {
 
    private Person object = new Person();
 
    public Person build() {
        return object;
    }
 
    public PersonBuilder setName(java.lang.String value) {
        object.setName(value);
        return this;
    }
 
    public PersonBuilder setAge(int value) {
        object.setAge(value);
        return this;
    }
}
自定义的注解如下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {

}

自定义的注解处理器如下:

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.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/5/27 11:21
 */
@SupportedAnnotationTypes(value = {"club.throwable.processor.builder.Builder"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement typeElement : annotations) {
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);
            Map<Boolean, List<Element>> annotatedMethods
                    = annotatedElements.stream().collect(Collectors.partitioningBy(
                    element -> ((ExecutableType) element.asType()).getParameterTypes().size() == 1
                            && element.getSimpleName().toString().startsWith("set")));
            List<Element> setters = annotatedMethods.get(true);
            List<Element> otherMethods = annotatedMethods.get(false);
            otherMethods.forEach(element ->
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "@Builder must be applied to a setXxx method "
                                    + "with a single argument", element));
            Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
                    setter -> setter.getSimpleName().toString(),
                    setter -> ((ExecutableType) setter.asType())
                            .getParameterTypes().get(0).toString()
            ));
            String className = ((TypeElement) setters.get(0)
                    .getEnclosingElement()).getQualifiedName().toString();
            try {
                writeBuilderFile(className, setterMap);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private void writeBuilderFile(
            String className, Map<String, String> setterMap)
            throws IOException {
        String packageName = null;
        int lastDot = className.lastIndexOf('.');
        if (lastDot > 0) {
            packageName = className.substring(0, lastDot);
        }
        String simpleClassName = className.substring(lastDot + 1);
        String builderClassName = className + "Builder";
        String builderSimpleClassName = builderClassName
                .substring(lastDot + 1);

        JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);

        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

            if (packageName != null) {
                out.print("package ");
                out.print(packageName);
                out.println(";");
                out.println();
            }
            out.print("public class ");
            out.print(builderSimpleClassName);
            out.println(" {");
            out.println();
            out.print("    private ");
            out.print(simpleClassName);
            out.print(" object = new ");
            out.print(simpleClassName);
            out.println("();");
            out.println();
            out.print("    public ");
            out.print(simpleClassName);
            out.println(" build() {");
            out.println("        return object;");
            out.println("    }");
            out.println();
            setterMap.forEach((methodName, argumentType) -> {
                out.print("    public ");
                out.print(builderSimpleClassName);
                out.print(" ");
                out.print(methodName);

                out.print("(");

                out.print(argumentType);
                out.println(" value) {");
                out.print("        object.");
                out.print(methodName);
                out.println("(value);");
                out.println("        return this;");
                out.println("    }");
                out.println();
            });
            out.println("}");
        }
    }
}

主类如下:

public class Main {

    public static void main(String[] args) throws Exception{
      //PersonBuilder在编译之后才会生成,这里需要编译后才能这样写
      Person person  = new PersonBuilder().setAge(25).setName("doge").build();
    }
}

先手动编译BuilderProcessor,然后在META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.builder.BuilderProcessor,最后执行Maven命令mvn compile进行编译。

编译后控制台输出:

[errorRaised=false, rootElements=[club.throwable.processor.builder.PersonBuilder], processingOver=false]
编译成功之后,target/classes包下面的club.throwable.processor.builder子包路径中会新增了一个类PersonBuilder:

package club.throwable.processor.builder;

public class PersonBuilder {
    private Person object = new Person();

    public PersonBuilder() {
    }

    public Person build() {
        return this.object;
    }

    public PersonBuilder setName(String value) {
        this.object.setName(value);
        return this;
    }

    public PersonBuilder setAge(Integer value) {
        this.object.setAge(value);
        return this;
    }
}

这个类就是编译期新增的。在这个例子中,编译期新增的类貌似没有什么作用。但是,如果像lombok那样对原来的实体类添加新的方法,那样的话就比较有用了。因为些类或者方法是编译期添加的,因此在代码中直接使用会标红。因此,lombok提供了IDEA或者eclipse的插件,插件的功能的实现估计也是用了插件式注解处理API。

小结

我在了解Pluggable Annotation Processing API的时候,通过搜索引擎搜索到的几乎都是安卓开发通过插件式注解处理API编译期动态添加代码等等的内容,可见此功能的使用还是比较广泛的。可能在文中的实战例子并不能体现Pluggable Annotation Processing API功能的强大,因此有时间可以基于此功能编写一些代码生成插件,例如lombok。

jsr规范系列(2)——javase规范、javaee规范、jsr规范全面整理——截止201912_ni_hao_fan的博客-爱代码爱编程_java jsr

目录 JCP组织和JSR规范符合JSR规范的框架JavaSE规范JavaEE规范 网上找不到详细的资料,劳资自己动手写一篇~ 前面写了Java版本、JSR规范和JCP社区流程概述,接下来看看JSR规范有哪些。

jdk6.0的新特性之六:插入式注解处理api(pluggable annotation processing api)-爱代码爱编程

作者: 飞翼 发表日期: 2007-01-03 20:10 插入式注解处理API(JSR 269)提供一套标准API来处理Annotations(JSR 175),实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method, package

jsr 269实践-爱代码爱编程

一、背景 如果你有使用过lombok和mapStruct等类似插件,对其实现原理好奇,那这篇文章可以帮助你了解它们的实现原理和套路。 二、JSR 269 JSR 269: Pluggable Annotation Pr

由lombok说起,浅析JSR-269原理及应用-爱代码爱编程

lombok简介 在Java语言的项目开发中,存在着大量的样板代码。如实体类中大量的setter,getter,equals,HashCode,toString方法,即使idea可以自动快捷帮我们生成这些方法,但在增减字段时仍然需要重新去维护这些方法;又比如各种IO流等资源的关闭,try…catch…finally模式如此经典以至于成为了effectiv

注解(3):插件化注解处理API(Pluggable Annotation Processing API)-爱代码爱编程

    插件化注解处理(Pluggable Annotation Processing)APIJSR 269提供一套标准API来处理AnnotationsJSR 175,实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method、package、constructor、type、var

【JSR269实战】之编译时操作AST,修改字节码文件,以实现和lombok类似的功能-爱代码爱编程

笔者日常: 兄弟姐妹们,还是尽量少熬夜啊。我感觉我记性有所下降,难受。 需求说明(本文以实现此需求为例进行说明):   现在有一个需求,就是要给枚举类生成一个内部类,这个内部类中以静态常量的形式记录外部枚举类所有枚举项的值,即: 编译前java文件是这样的:(编译时操作AST,)编译后的class文件是这样的:编译时操作AST,修改字节码文件: 软

研究Java 9 Money and Currency API(JSR 354)-爱代码爱编程

JSR 354定义了一个用于处理货币和货币的新Java API,计划将其包含在Java 9中。在本文中,我们将研究参考实现的当前状态: JavaMoney 。 就像我关于Java 8日期/时间API的帖子一样,该帖子将主要由显示新API的代码驱动。 但是在开始之前,我想引用一下规范中的一小部分,以总结一

Lombok注解-理解版-爱代码爱编程

一、前沿 lombok是什么? Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method

插入式注解API(Pluggable Annotation Processing API)-爱代码爱编程

   在JDK 1.5之后,Java语言提供了对注解(Annotation)的支持,这些注解与普通的Java代码一样,是在运行期间发挥作用的。在JDK 1.6中实现了JSR-269规范JSR-269:Pluggable Annotations Processing API(插入式注解处理API)。提供了一组插入式注解处理器的标准API在编译期间对注解进行处

对象转换神器-mapstruct_有梦想的泡椒猪蹄的博客-爱代码爱编程

MapStruct是一个代码生成器,它基于约定优于配置方法极大地简化了Java bean类型之间映射的实现。 生成的映射代码使用普通方法调用,因此快速,类型安全且易于理解。 如何接入MapStruct 官网文档IDEA Support: https://plugins.jetbrains.com/plugin/10036-mapstruct-suppo

【idea】java jsr269 注解处理器快速上手_frms的博客-爱代码爱编程

实现方法 简介正文其他 简介 注意:这篇文章旨在提供快速实现注解处理器(JSR 269)在Idea项目正确运行方法,不会讲解所涉及的理论知识。 正文 使用idea新建项目,用maven模块,选择的java

java jsr-269 插入式注解处理器_spring呀的博客-爱代码爱编程

文章目录 JSR-269 & 什么是插入式注解处理器1、快速开始2、语法树相关简介2.1、JCTree2.2、TreeMaker2.2.1、TreeMaker.Modifiers2.2.2、TreeMaker