注解处理器(Annotation Processor)简析

作者: Whyn | 来源:发表于2017-04-04 00:26 被阅读4837次

    概念

    注解处理器(Annotation Processor)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容。

    用途

    由于注解处理器可以在程序编译阶段工作,所以我们可以在编译期间通过注解处理器进行我们需要的操作。比较常用的用法就是在编译期间获取相关注解数据,然后动态生成.java源文件(让机器帮我们写代码),通常是自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率。

    例子

    butterknifeDagger2EventBus......

    Annotation Processor实质原理

    ** 编译期间根据注解(Annotation)获取相关数据 **

    既然Annotation Processor是为了在编译期间获取注解(Annotation)相关内容,那么,具体的操作步骤要如何做呢:

    1. Android Studio创建一个java library
    2. 自定义一个注解(Annotation),用于存储元数据
    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.FIELD)
    public @interface BindView {
        int value() default -1;
    }
    
    1. 创建一个自定义Annotation Processor继承于AbstractProcessor
    package com.example;
    
    @AutoService(Processor.class)
    public class MyProcessor extends AbstractProcessor {
    
        @Override
        public synchronized void init(ProcessingEnvironment env){
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv) { }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() { 
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
        }
    }
    
    • @AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
      AutoService这里主要是用来生成
      META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册,具体手动注册方法如下:
      1.创建一个
      META-INF/services/javax.annotation.processing.Processor文件,
      其内容是一系列的自定义注解处理器完整有效类名集合,以换行切割:
    com.example.MyProcessor
    com.foo.OtherProcessor
    net.blabla.SpecialProcessor
    

    2.将自定义注解处理器和
    META-INF/services/javax.annotation.processing.Processor打包成一个.jar文件。所以其目录结构大概如下所示:

    MyProcessor.jar
        - com
            - example
                - MyProcessor.class
    
        - META-INF
            - services
                - javax.annotation.processing.Processor
    

    *** 建议直接采用@AutoService(Processor.class)进行自定义注解处理器注册,简洁方便 ***

    • init(ProcessingEnvironment env):每个Annotation Processor必须***
      有一个空的构造函数 *。编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnviroment参数,通过该参数可以获取到很多有用的工具类: Elements , Types , Filer **等等
    • process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv):Annotation Processor扫描出的结果会存储进roundEnv中,可以在这里获取到注解内容,编写你的操作逻辑。注意,process()函数中不能直接进行异常抛出,否则的话,运行Annotation Processor的进程会异常崩溃,然后弹出一大堆让人捉摸不清的堆栈调用日志显示.
    • getSupportedAnnotationTypes(): 该函数用于指定该自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),注解(Annotation)指定必须是完整的包名+类名(eg:com.example.MyAnnotation)
    • getSupportedSourceVersion():用于指定你的java版本,一般返回:SourceVersion.latestSupported()。当然,你也可以指定具体java版本:
      return SourceVersion.RELEASE_7;
    1. 经过前面3个步骤后,其实就已经算完成了自定义Annotation Processor。后面要做的就是在源码里面,在需要的地方写上我们自定义的注解就行了。

    Demo

    牢记Annotation Process的实质用处就是在编译时通过注解获取相关数据,
    那么,在这个Demo里面,我们就直接在编译时打印出我们注解的数据的成员变量名,成员变量类,包装类类名,包名和注解元数据进行显示,然后将这些信息写入到一个.java文件中,这里我就简单的直接输出这些信息进行显示。
    按照上面自定义注解处理的方法,我们操作如下:

    1. 创建一个java library,其gradle配置如下:
    apply plugin: 'java'
    
    targetCompatibility = '1.7'
    sourceCompatibility = '1.7'
    
    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        compile 'com.google.auto.service:auto-service:1.0-rc3'
    }
    
    1. 自定义一个注解(Annotation),用于存储元数据
    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.FIELD)
    public @interface BindView {
        int value() default -1;
    }
    
    1. 创建一个自定义Annotation Processor继承于AbstractProcessor
    
    
    @AutoService(Processor.class)
    public class MyAnnotationProcessor extends AbstractProcessor {
    
        private Filer mFiler;
        private Messager mMessager;
        private Elements mElementUtils;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mFiler = processingEnvironment.getFiler();
            mMessager = processingEnvironment.getMessager();
            mElementUtils = processingEnvironment.getElementUtils();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotations = new LinkedHashSet<>();
            annotations.add(BindView.class.getCanonicalName());
            return annotations;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            for (Element element : bindViewElements) {
                //1.获取包名
                PackageElement packageElement = mElementUtils.getPackageOf(element);
                String pkName = packageElement.getQualifiedName().toString();
                note(String.format("package = %s", pkName));
    
                //2.获取包装类类型
                TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
                String enclosingName = enclosingElement.getQualifiedName().toString();
                note(String.format("enclosindClass = %s", enclosingElement));
    
    
                //因为BindView只作用于filed,所以这里可直接进行强转
                VariableElement bindViewElement = (VariableElement) element;
                //3.获取注解的成员变量名
                String bindViewFiledName = bindViewElement.getSimpleName().toString();
                //3.获取注解的成员变量类型
                String bindViewFiledClassType = bindViewElement.asType().toString();
    
                //4.获取注解元数据
                BindView bindView = element.getAnnotation(BindView.class);
                int id = bindView.value();
                note(String.format("%s %s = %d", bindViewFiledClassType, bindViewFiledName, id));
    
                //4.生成文件
                createFile(enclosingElement, bindViewFiledClassType, bindViewFiledName, id);
                return true;
            }
            return false;
        }
    
        private void createFile(TypeElement enclosingElement, String bindViewFiledClassType, String bindViewFiledName, int id) {
            String pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
            try {
                JavaFileObject jfo = mFiler.createSourceFile(pkName + ".ViewBinding", new Element[]{});
                Writer writer = jfo.openWriter();
                writer.write(brewCode(pkName, bindViewFiledClassType, bindViewFiledName, id));
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
        private String brewCode(String pkName, String bindViewFiledClassType, String bindViewFiledName, int id) {
            StringBuilder builder = new StringBuilder();
            builder.append("package " + pkName + ";\n\n");
            builder.append("//Auto generated by apt,do not modify!!\n\n");
            builder.append("public class ViewBinding { \n\n");
            builder.append("public static void main(String[] args){ \n");
            String info = String.format("%s %s = %d", bindViewFiledClassType, bindViewFiledName, id);
            builder.append("System.out.println(\"" + info + "\");\n");
            builder.append("}\n");
            builder.append("}");
            return builder.toString();
        }
    
    
        private void note(String msg) {
            mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
        }
    
        private void note(String format, Object... args) {
            mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(format, args));
        }
    
    }
    
    

    ** 借助Messager,我们可以在编译时输出日志. **

    1. 使用注解,我们在Android工程中创建几个测试类,然后进行注解,如下所示:
    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.tv)
        TextView tv;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    
    

    rebuild一下,可以在Gradle Console窗口中看到打印结果:


    result

    可以看到,我们成功的在编译期间获取了我们注解的相关数据。只要拿到了数据,那么你自己想干嘛就自己去弄吧 _
    最后,我们根据注解获取到的数据还生成了一个java文件,其生成路径:app\build\generated\source\apt\debug\com\yn\annotationprocessdemo\ViewBinding.java
    具体内容如下:

    package com.yn.annotationprocessdemo;
    
    //Auto generated by apt,do not modify!!
    
    public class ViewBinding {
    
        public static void main(String[] args) {
            System.out.println("android.widget.TextView tv = 2131427422");
        }
    }
    

    附录:

    • @AutoService引入:
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    
    • app的gralde配置:
          apply plugin: 'com.android.application'
    
          android {
              compileSdkVersion 24
              buildToolsVersion "24.0.0"
    
              defaultConfig {
                  applicationId "com.example.annotationprocessor"
                  minSdkVersion 15
                  targetSdkVersion 24
                  versionCode 1
                  versionName "1.0"
              }
               buildTypes {
                  release {
                      minifyEnabled false
                      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                  }
              }
    
              compileOptions {
                  sourceCompatibility JavaVersion.VERSION_1_7
                  targetCompatibility JavaVersion.VERSION_1_7
              }
              //解决duplicate问题
              packagingOptions {
                  exclude 'META-INF/services/javax.annotation.processing.Processor'
              }
              }
              dependencies {
              compile fileTree(dir: 'libs', include: ['*.jar'])
              testCompile 'junit:junit:4.12'
              compile 'com.android.support:appcompat-v7:24.0.0'
              compile project(path: ':annotationprocessor')
              }
    

    相关文章

      网友评论

      • 技术萌新:按照你的步骤,但是AbstractProcessor就是不执行,也不报错
        技术萌新:能加下qq吗,476345149,我把代码发你,帮忙看下
        技术萌新:@Whyn 一模一样
        Whyn:源码可以查看:https://github.com/Why8n/AnnotationProcessDemo.git
      • 望北8261:学习了
      • Edgar_Ng:谢谢,哥们的例子可以用!
      • messiGao:如何通过注解找到对应的注解处理类processor。看spring源码有时找不到。
        Whyn:??spring源码我不清楚,但是搜索这个注解出现的位置不就行了吗?
      • 鈤汌冈板:麻烦问一下,我按照上面代码在主工程可以运行,但是读取不到其他module的注解信息。我在工程中新建了几个library,为啥注解信息读不到呢?依赖已经正确添加了~~~谢谢 麻烦解答下...
        鈤汌冈板:@Whyn 解决了,选择是文件名重复了。我改成用包名+文件名创建文件就可以了!谢谢作者关注!
        Whyn:是否是其他module没有引入注解处理器库。
      • 冉桓彬:顺便问一下, 你github上面的demo可以打印出来日志吗?
        Whyn:@冉桓彬 直接用github的那个库没有生成?rebuild一下试下。
        冉桓彬:@Whyn 我运行以后, ViewBInding.java也没有生成
        Whyn:@冉桓彬 有的呀!博文里面都打出来了😹
      • 冉桓彬:你遇到过plugin with id "java-library" not found这个问题了吗?
        94d8213d866d:我猜你是直接在android项目里面写的。
      • GordenNee:不错
      • 5188042f2246:compile project(path: ':annotationprocessor') 找不到这个
        博主能开源出 代码 吗 .
        按照教程写的时候少个东西,对初学者不厚道.
        辛苦
        冉桓彬:@Whyn 谢谢
        5188042f2246:@Whyn 谢谢
        Whyn:annotationprocessor就是你的自定义注解库module。
        源码可以查看:https://github.com/Why8n/AnnotationProcessDemo.git

      本文标题:注解处理器(Annotation Processor)简析

      本文链接:https://www.haomeiwen.com/subject/xbooottx.html