代码记忆

插拔式处理注解Api及仿lombok Getter注解实现

2019-01-30  本文已影响175人  Acamy丶

前两篇文章分别分析了基于Java Agent的premain和attach方式来修改字节码,premain是在类加载前修改,attach是在类加载后修改,本文继续讲字节码的修改,只不过修改的时间是在更早的编译阶段。通过使用插拔式注解处理API(Pluggable Annotation Processing API, JSR 269)可以让我们定义的注解在编译期而非运行期生效,从而达到在编译期修改字节码的目的。当前非常流行的lombok框架就是使用该特性来实现,在项目中我们通过引入lombok的依赖和安装ide插件即可使用其提供的注解大大简化代码的开发,本文通过实现一个Getter注解来说明其工作原理。

如下图所示,本文要实现的Getter注解最终目标就是让这段有"语法错误"的代码能够通过编译并运行,也就是让使用该注解的类能够自动生成get方法。

image-20190130194202335

为了方便测试类的使用,我们将实现Getter功能的代码写在一个单独的工程并打成jar包并提交到自己本地的maven项目,完整的的项目结构如下:

getterbok
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── hebh
        │           ├── Getter.java
        │           └── GetterProcessor.java
        └── resources
            ├── META-INF
            │   └── services
            │       └── javax.annotation.processing.Processor
            └── log4j2.xml

相关依赖,除了日志外还要引入java自带的tools包

<dependencies>
    <!--引入系统路径的tools jar包-->
    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.8</version>
      <scope>system</scope>
      <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>

    <!--log4j2日志依赖-->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.11.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.11.1</version>
    </dependency>
  </dependencies>

build部分, 自身项目在编译前并没有Processor的class文件且也不需要用到,因此在编译期要过滤Processor文件并且在打包前再拷回来

  <build>
    <resources>
      <!--编译时过滤掉Processor文件-->
      <resource>
        <directory>src/main/resources</directory>
        <excludes>
          <exclude>META-INF/**/*</exclude>
        </excludes>
      </resource>
    </resources>

    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>2.6</version>
        <executions>
          <execution>
            <id>process-META</id>
            <!--打包前再将文件拷贝过来-->
            <phase>prepare-package</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>target/classes</outputDirectory>
              <resources>
                <resource>
                  <directory>${basedir}/src/main/resources/</directory>
                  <includes>
                    <include>**/*</include>
                  </includes>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

Getter注解类的定义, 限定其使用范围和生效时期

@Target({ElementType.TYPE}) // 使用在类上
@Retention(RetentionPolicy.SOURCE) //表示这个注解只在编译期起作用
public @interface Getter {
}

继承AbstractProcessor的GetterProcessor类,限定要处理哪些注解和源码级别,该类也是实现功能的核心类,通过重写process方法来对字节码进行修改。

@SupportedAnnotationTypes("com.hebh.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
    private static final Logger logger = LogManager.getLogger(GetterProcessor.class);

    private Messager messager;
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        logger.debug("Enter method init");
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public synchronized boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if(annotations.size() > 0){
            logger.debug("Enter method process, {}, {}", annotations, roundEnv.getRootElements());
        }
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                    for (JCTree tree : jcClassDecl.defs) {
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }

                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        logger.debug( "{} has been processed", jcVariableDecl.getName());
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }

            });
        });

        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
    }

    /**
     * 获取新方法名,get + 将第一个字母大写 + 后续部分, 例如 value 变为 getValue
     * @param name
     * @return
     */
    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
}

javax.annotation.processing.Processor,采用Java的SPI(Service Provider Interface)机制,放在META-INF/services文件夹下面, 以接口全路径为名,实现类全路径为内容,而在程序运行时能够动态为接口替换实现类。

com.hebh.GetterProcessor

Log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-3level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

以上就是全部代码的实现,然后使用mvn cean install将该项目提交到本地maven仓库。

然后在测试项目中引入上一步生成的jar包:

  <dependencies>
    <dependency>
      <groupId>com.hebh</groupId>
      <artifactId>getterbok-demo</artifactId>
      <version>0.0.1</version>
    </dependency>
  </dependencies>

在测试项目中执行编译命令mvn compile,从如下打印日志中可以看出我们的Getter注解已经生效了

image-20190130201517269

看看idea反编译.class文件的源码:

image-20190130201944544

可以看到已经生成了getValue方法, 并且已经没有了Getter方法。

在target目录执行运行命令java com.hebh.App, 顺利打印出字符串。

image-20190130202346830

目标达成。。。

上一篇下一篇

猜你喜欢

热点阅读