zoukankan      html  css  js  c++  java
  • 注解(二)- 自定义注解处理器

    标签: java annotation


    上一篇博客讨论了关于注解的基础知识,以及运行时(Runtime)通过反射机制来处理注解,但既然是Runtime,那么总会有效率上的损耗,如果我们能够在编译期(Compile time)就能处理注解,那自然更好,而很多框架其实都是在编译期处理注解,比如大名鼎鼎的bufferknife,这个过程并不复杂,只需要我们自定义注解处理器(Annotation Processor)就可以了。(Annotation Processor下文有些地方直接简称处理器,不要理解成cpu那个处理器)。

    在Compile time注解就能起作用,这才是真正体现注解价值的地方,不过自定义Compile time的注解处理器也没什么神秘的。注解处理器是编译器(javac)的一个工具,它用来在编译时扫描和处理注解。我们可以自定义一个注解,并编写和注册对应的处理器。在写法上它其实就是我们自定义一个类,该类 extends javax.annotation.processing.AbstractProcessorAbstractProcessor是一个abstract的基类。它以我们写好的java源码或者编译好的代码做为输入,然后就可以通过处理器代码来实现我们所希望的输出了,比如输出一份新的java代码,此时注解管理器就以递归的形式进行多趟处理,直到把代码(包括你手写的代码,以及注解处理器生成的代码)中所有的注解都被处理完毕。

    我们已经写好的代码固然是不能修改了,但是这并不影响通过注解处理器来生成新的代码。还以bufferknife为例,写findViewById实在太无聊了,所以我们就使用了bufferknife的注解方式省略这个过程。

    public class TestMainActivity extends BaseActivity {
        @BindView(R.id.mainSwitchGoneBtn)
        Button goneBtn;
        .......
    }
    

    但是实际上呢,是bufferknife通过其注解处理器器来生成了相应的代码,它生成的文件是这样的:

    public class TestMainActivity_ViewBinding<T extends TestMainActivity> implements Unbinder {
      protected T target;
    
      @UiThread
      public TestMainActivity_ViewBinding(T target, View source) {
        this.target = target;
    
        target.goneBtn = Utils.findRequiredViewAsType(source, R.id.mainSwitchGoneBtn, "field 'goneBtn'", Button.class);
      }
    }
    

    所以bufferknife就是通过这种方式来麻烦了自己,方便了我们。

    注解处理器是运行在它自己的虚拟机jvm当中的,也就是说,javac启动了一个完整的java虚拟机来运行注解处理器,这点非常重要,因为这说明你编写的注解处理器代码,和你写的其他java代码是没什么区别的。不管是你使用的API,还是设计时的思想,编码习惯,甚至你想使用的其他第三方类库,框架等,都是一样的。

    认识处理器

    前面就说过,我们自定义的过程,就是extends AbstractProcessor,先来看看这个抽象处理器类。

    package com.yaoxiaowen.testprocessor;
    
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.TypeElement;
    
    public class TestProcessor extends AbstractProcessor{
    	
    	/**
    	 * 每个注解处理器都必须有一个空的构造方法(父类已经实现了),这个init方法会被构造器调用,
    	 * 并传入一个 ProcessingEnvironment 参数,该参数提供了很多工具类,
    	 * 比如 Elements, Filer, Messager, Types
    	 * @author www.yaoxiaowen.com
    	 */
    	@Override
    	public synchronized void init(ProcessingEnvironment env) {
    		// TODO Auto-generated method stub
    		super.init(env);
    	}
    
    	/**
    	 * 这个方法在父类中是abstract的,所以子类必须实现。
    	 * 这个方法就是相当于 注解处理器的 入口 main()方法,我们说在编译时,对注解进行的处理,
    	 * 比如对注解的扫描,评估和处理,以及后续的我们要做的其他操作。(比如生成其他java代码文件),
    	 * 都是在这里发生的。
    	 * 
    	 * 参数RoundEnvironment可以让我们查出包含特定注解的被注解元素。
    	 * @author www.yaoxiaowen.com
    	 */
    	@Override
    	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    		// TODO Auto-generated method stub
    		return false;
    	}
    
    	/**
    	 * 这个方法虽然在父类当中不是 abstract的,但是我们也必须实现。
    	 * 因为该方法的作用是指定我们要处理哪些注解的,
    	 * 比如你想处理注解MyAnnotation,可是该处理器怎么知道你想处理MyAnnotation,而不是OtherAnnotation呢。
    	 * 所以你要在这里指明,你需要处理的注解的全称。
    	 * 
    	 * 返回值是一个字符串的集合,包含着本处理器想要处理的注解类型的合法全称。
    	 * @author www.yaoxiaowen.com
    	 */
    	@Override
    	public Set<String> getSupportedAnnotationTypes() {
    		// TODO Auto-generated method stub
    		return super.getSupportedAnnotationTypes();
    	}
    
    	/**
    	 * 本方法用来指明你支持的java版本,
    	 * 不过一般使用 SourceVersion.latestSupported() 就可以了。 
    	 */
    	@Override
    	public SourceVersion getSupportedSourceVersion() {
    		// TODO Auto-generated method stub
    		return super.getSupportedSourceVersion();
    	}
    }
    

    这几个主要方法,在代码片段的注释已经写的很清楚了。

    我们使用TestProcessor.java这个处理器的目的就是分析处理java代码,而代码是遵循一定的结构规范的,代码文件被读取后,各个字符串会被分解成token进行处理,而javac的编译器首先将java代码分解为抽象语法树(AST)。而这个结构,在处理器内部,其实是被表示成这样的:

    package com.example;    // PackageElement
    
    public class Foo {        // TypeElement
    
        private int a;      // VariableElement
        private Foo other;  // VariableElement
    
        public Foo () {}    // ExecuteableElement
    
        public void setA (  // ExecuteableElement
            int newA   // TypeElement
        ){}
    }
    

    处理器在处理代码时,其实就是对抽象语法树进行遍历操作,分解出每一个的类,方法,属性等,然后再将这些元素的内容进行处理。

    而实际上,这些PackageElement,VariableElement等元素模型都是在一个专门的类包中javax.lang.modeljavax.lang.model用来为 Java 编程语言建立模型的包的类和层次结构。 此包及其子包的成员适用于语言建模、语言处理任务和 API(包括但并不仅限于注释处理框架)。

    继承 AbstractProcessor实现自定义处理器

    我们现在通过继承AbstractProcessor来实现一个小demo。
    流程和功能如下:我们定义了一个注解SQLString,然后实现注解处理器 DbProcessor。该注解处理器功能很简单,就是生成一个文件,将实现了SQLString的属性元素的相关内容写入到这个文件(比如所在类的名字,属性名,所设置的注解的值)。

    我们先自定义一个注解

    package com.yaoxiaowen.comp.proce.db;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import javax.lang.model.element.Element;
    
    /**
     * 该注解的 使用范围是 属性(域) 上
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface SQLString {
    	int value() default 0;
    	String name() default "";
    }
    

    然后再来定义注解处理器

    package com.yaoxiaowen.comp.proce.db;
    
    import java.io.File;
    import java.io.FileWriter;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeSet;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.element.VariableElement;
    import javax.tools.Diagnostic;
    
    /**
      * @author www.yaoxiaowen.com
      */
    public class DbProcessor extends AbstractProcessor{
    	private Messager messager;
    	
    	private int count = 0;
    	private int forCount = 0;
    	private StringBuilder generateStr = new StringBuilder();
    	
    	@Override
    	public synchronized void init(ProcessingEnvironment env) {
    		// TODO Auto-generated method stub
    		super.init(env);
    		messager = env.getMessager();
    		String logStr = "enter init(),  进入 init()";
    		printMsg(logStr);
    	}
    
    	@Override
    	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    		// TODO Auto-generated method stub
    		String logStr = "enter process(), 进入process";
    		
    	
    		//用来 存储 (className, 输出语句) 这种结构 
    		Map<String, String> maps = new HashMap<>();
    		
    		//得到 使用了 SQLString注解的元素
    		Set<? extends Element> eleStrSet = env.getElementsAnnotatedWith(SQLString.class);
    		
    		count++;
    		
    		for (Element eleStr : eleStrSet){
    			
    			//因为我们知道SQLString元素的使用范围是在域上,所以这里我们进行了强制类型转换
    			VariableElement eleStrVari = (VariableElement)eleStr;
    			forCount++;
    			
    			// 得到该元素的封装类型,也就是 包裹它的父类型
    			TypeElement enclosingEle = (TypeElement)eleStrVari.getEnclosingElement();
    			String className = enclosingEle.getQualifiedName().toString();
    			
    			generateStr.append("className = " + className);
    			generateStr.append("	 fieldName = " + eleStrVari.getSimpleName().toString());
    			
    			//得到在元素上,使用了注解的相关情况
    			SQLString sqlString = eleStrVari.getAnnotation(SQLString.class);
    			generateStr.append("	 annotationName = " + sqlString.name());
    			generateStr.append("	 annotationValue = " + sqlString.value());
    			generateStr.append("	 forCount=" + forCount);
    			generateStr.append("
    ");
    		}
    		
    		generateStr.append("test File yaowen");
    		generateStr.append("	 count=" + count);
    		generateFile(generateStr.toString());
    		return true;
    	}
    
    	@Override
    	public Set<String> getSupportedAnnotationTypes() {
    		// TODO Auto-generated method stub
    		Set<String> strings = new TreeSet<>();
    		strings.add("com.yaoxiaowen.comp.proce.db.SQLString");
    		return strings;
    	}
    
    	@Override
    	public SourceVersion getSupportedSourceVersion() {
    		// TODO Auto-generated method stub
    		return SourceVersion.latestSupported();
    	}
    	
    	//将内容输出到文件
    	private void generateFile(String str){
    		try {
    			//这是mac环境下的路径
    			File file = new File( "/Users/yw/code/dbCustomProcFile");
    			FileWriter fw = new FileWriter(file);
    			fw.append(str);
    			
    			fw.flush();
    			fw.close();
    			
    		} catch (Exception e) {
    			// TODO: handle exception
    			e.printStackTrace();
    			printMsg(e.toString());
    		}
    	}
    	
    	private void printMsg(String msg){
    		messager.printMessage(Diagnostic.Kind.ERROR, msg);
    	}	
    }
    

    结合着注释,我们知道,这个处理器的功能就是将一些信息输出到 /Users/yw/code/dbCustomProcFile 这个文件中。

    我在代码中使用了 javax.annotation.processing.Messager来输出一些log信息,因为这个过程是在编译时输出的,所以System.out.println()就没用了,这个输出信息是给使用了该处理器的第三方程序员看的,不是给该处理器的作者看的。
    比如demo当中的log代码,在最后成功的打包成jar,在另一个项目中使用时(Android Studio环境下,Eclipse我愣是没找到哪个窗口输出编译信息),编译时期输出信息如下:

    .......
    :app:compileSc_360DebugJavaWithJavac
    注: enter init, 进入init
    注: enter process, 进入process
    注: Creating DefaultRealmModule
    注: enter process, 进入process
    注: enter process, 进入process
    注: 某些输入文件使用了未经检查或不安全的操作。
    注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
    
    :app:generateJsonModelSc_360Debug UP-TO-DATE
    :app:externalNativeBuildSc_360Debug
    ......
    

    添加注册信息

    处理器的代码虽然写完了,但是这还没完呢,剩下还有非常重要的步骤,那就是添加注册信息。因为注解处理器是属于javac的一个平台级的功能,所以我们的使用方式是将代码打包成jar的形式,这样就可以在其他第三方项目当中使用了。而在打包jar之前,则要在项目中添加注册信息。

    先看一下这个目录的结构:

    (eclipse)注册的步骤如下:
    1,选中工程,鼠标右键,New -> Source Folder,创建 resources文件夹,然后依次通过New -> Folder 创建两个文件夹 : META-INF,services
    2,在services文件夹下,New -> File, 创建一个文件,javax.annotation.processing.Processor。在文件中,输入自定义的处理器的全名:
    com.yaoxiaowen.comp.proce.db.DbProcessor
    输入之后记得键入回车一下。

    其实这个手动注册的过程,也是可以不用我们麻烦的。google开发了一个注解工具AutoService,我们可以直接在处理器代码上使用。类似下面这样:

    /**
      * @author www.yaoxiaowen.com
      */
    @AutoService(Processor.class)
    public class DbProcessor extends AbstractProcessor{
            .......
    

    这个注解工具自动生成META-INF/services/javax.annotation.processing.Processor文件,文件里还包含了处理器的全名:
    com.yaoxiaowen.comp.proce.db.DbProcessor

    看到这里,你也许会比较震惊,我们在注解处理器的代码中也可以使用注解。
    那么此时请再看看本文开头的那句话

    注解处理器是运行在它自己的虚拟机jvm当中的,也就是说,javac启动了一个完整的java虚拟机来运行注解处理器.....

    做完这些,我们的项目就已经完成了,下面要做的就是打包成jar了。

    打包和使用jar(eclipse为例)

    1: 打包jar
    前面说过,编译期的注解处理器是平台级的功能,是要注册给javac的, 所以需要打包成jar, 我们的项目打包的名字是
    AnnoCustomProce.jar

    关于具体的打包过程,参见gif图(这是从鸿洋大神的博客上学习到的)。

    2: 建立新项目
    eclipse下的java项目,新建立一个lib文件夹,然后将AnnoCustomProce.jar手动拷贝到这个目录下。

    3: 引用包,并启用annotation processor。
    具体操作见gif图。

    要注意一下两个gif图中的各种选项和配置。

    使用注解

    现在呢,已经大功告成了。下面就是使用了。
    新建一个类,使用我们的注解。

      public class AnnoCreateFile {
    
    	@SQLString(name="yw")
    	String filed;
    	@SQLString(name="yaow", value=1)
    	String name;
    	
    	/**
    	 * @author www.yaoxiaowen.com
    	 */
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		System.out.println("hello world");
    	}
    }
    

    当编译完这个项目时(eclipse默认就是Build Automatically),我们就能在 /Users/yw/code/目录下找到 dbCustomProcFile 文件了,打开这个文件,内容如下:

    className = com.yaoxiaowen.testjar.AnnoCreateFile	 fieldName = filed	 annotationName = yw	 annotationValue = 0	 forCount=1
    className = com.yaoxiaowen.testjar.AnnoCreateFile	 fieldName = name	 annotationName = yaow	 annotationValue = 1	 forCount=2
    test File yaowen	 count=1test File yaowen	 count=2
    

    大功告成,我们成功的实现了一个能够在编译时期起作用的自定义注解处理器。

    当然,这个demo没有什么实际作用,它的功能也非常简单,但是了解了这个过程,我们在实际需求当中,就可以通过类似的方式来实现想要的功能了。

    很多时候,我们都是希望注解处理器是来输出java代码的,既然是代码,那么总有格式的,这就不像简单的文件那样进行输出了,输出java代码,一般使用一个类库:javapoet。而我们如果在注解处理器中引入了第三方的类库,那么将其打包成jar的过程,就和我们演示的有所不同。这点需要自行google。另外,如果想对java源码进行游刃有余的处理,那么需要对于javax.lang.model 包下的各种Elements,工具之类的比较熟悉。具体的api,需要参考oracle的文档.

    总结和反思

    我在学习自定义注解处理器的过程中,参考了网上的很多博客,敲代码进行实测,但是实际上依旧碰到了很多的问题,也折腾了好久,在这里我将自己所碰到的问题,都罗列出来。(虽然有些是可笑的低级错误),希望对大家有所帮助。

    1. resource/META-INF/services com.yaoxiaowen.annotation.createjson.BeanProcessor 文件中,写的是 处理器的类DbProcessor,而不是你的注解类:SQLString
    2. resource 这个文件夹 是 New Source Folder,后面两个文件,才是 New Folder, 关于两者之间的区别: 后者就是一个普通的文件夹而已,但是前者,是属于项目的一部分,eclipse会编译这个文件夹。所以有些文章说建立的 是res(而不是resource)文件夹,这个其实无所谓,只要它是 Souce Folder.
    3. 在导入包的时候,注意不要导入错误的包(比如import java.awt.Window.Type)。因为往往同一个类名,它在不同的包里都有实现。像常用的List,经常导错包。java.awt.Listjava.util.List List比较常用,我们很容易找到错误,但是 Type之类的不常用,所以不是那么容易发现。
    4. 在处理器的生成文件的代码中,有这样一句:
      File file = new File( "/Users/yw/code/dbCustomProcFile");
      
      那如果我将代码改成这样:
      File file = new File("./dbCustomProcFile");
      
      那么请问此时,这个dbCustomProcFile文件到底在那里呢?
      在我的电脑上,该文件路径分别如下
      D:softwareeclipsedbCustomProcFile(window)或/Users/yw//Downloads/Eclipse.app/Contents/MacOS/dbCustomProcFile(mac)。
      一般我们在工程中使用./我们都认为是工程当前的目录,但是在注解处理器中,这个路径实际上是eclipse 安装路径下 javac的路径。的确应该如此,因为注解处理器毕竟是javac的一个工具。
    5. 某一次测试时,当我向新工程导入生成的jar包时,刚刚导入eclipse就报错。

      后来终于发现了问题所在。

      在编码过程中,函数的行参原来是processingEnv,后来我嫌长,就修改为了env,但是下面一句super.init()却忘记修改了。所以导致找不到这个参数,而问题是在window和mac下,这句代码IDE都没有任何提示,我又在下面故意写了一句错代码String = 2,此时IDE提示错误。 那为什么第一句话IDE就是没报错呢。测试了普通java文件的同种类型的错误,IDE就会报错,所以这真的让我不解。

    以上就是本篇文章的全部内容,关于注解处理器深处的很多东西其实也没搞懂。也欢迎大家留言指点交流。


    作者: www.yaoxiaowen.com

    github: https://github.com/yaowen369

    欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系

    <p >
    		 欢迎转载,转载请注明出处.谢谢
    </p>
    
    
    <script type="text/javascript">
     function    Curgo()   
     {   
         window.open(window.location.href);
     }   
    </script>
    
  • 相关阅读:
    数据分析系统DIY1/3:CentOS7+MariaDB安装纪实
    NSArray与NSString、NSData,NSDictionary与NSString、NSData 相互转化
    Geek地生活,文艺地思考
    Android开发中遇到的问题(五)——Eclipse导入Android项目出现"Invalid project description overlaps the location of another project"错误的解决办法
    Android开发中遇到的问题(四)——Android中WARNING: Application does not specify an API level requirement!的解决方法
    Android开发中遇到的问题(三)——eclipse创建android项目无法正常预览布局文件
    Android开发中遇到的问题(二)——新建android工程的时候eclipse没有生成MainActivity和layout布局
    Android开发学习总结(三)——appcompat_v7项目说明
    Android开发学习总结(二)——使用Android Studio搭建Android集成开发环境
    Android开发学习总结(一)——搭建最新版本的Android开发环境
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/6753964.html
Copyright © 2011-2022 走看看