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

前两篇文章分别分析了基于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包


    
    
      com.sun
      tools
      1.8
      system
      ${java.home}/../lib/tools.jar
    

    
    
      org.apache.logging.log4j
      log4j-api
      2.11.1
    
    
      org.apache.logging.log4j
      log4j-core
      2.11.1
    
  

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

  
    
      
      
        src/main/resources
        
          META-INF/**/*
        
      
    

    
      
        org.apache.maven.plugins
        maven-compiler-plugin
        3.1
        
          1.8
          1.8
        
      

      
        org.apache.maven.plugins
        maven-resources-plugin
        2.6
        
          
            process-META
            
            prepare-package
            
              copy-resources
            
            
              target/classes
              
                
                  ${basedir}/src/main/resources/
                  
                    **/*
                  
                
              
            
          
        
      
    
  

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 annotations, RoundEnvironment roundEnv) {
        if(annotations.size() > 0){
            logger.debug("Enter method process, {}, {}", annotations, roundEnv.getRootElements());
        }
        Set 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 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 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



    
        
            
        
    
    
        
            
        
    

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

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

  
    
      com.hebh
      getterbok-demo
      0.0.1
    
  

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

image-20190130201517269

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

image-20190130201944544

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

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

image-20190130202346830

目标达成。。。

你可能感兴趣的:(插拔式处理注解Api及仿lombok Getter注解实现)