内容简介:关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。本文章同时对一般 Java 项目和 Android 项目适用。诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太
关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。
本文章同时对一般 Java 项目和 Android 项目适用。
为何使用 Java 注解预处理
诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太 慢 了。
啥?反射不慢?来来来,一个 Activity 就用几十次反射,要不要和复制粘贴做一下对比?(手动阴险)
那反射这么慢,有没有什么办法?当然就是今天的主题了——代码生成: 让编译器来给你“复制粘贴”,既优雅,又高效(反正生成的代码你也不看)。
如何使用 Java 注解预处理
关于注解预处理的基本使用方法的资料还是很多的,这里就不细说了,概括一下就是:
javax.annotation.processing.AbstractProcessor META-INF.services.javax.annotation.processing.Processor
注意:对于 Android 项目,你需要单独建立一个 “Java 类” 项目,不可以直接在原 Android 项目中使用 注解预处理,否则你会发现没有 javax 这个包。
然后,在 Android 项目的 build.gradle
中的 dependencies
添加 annotationProcessor project(':项目名')
处理我们的注解
假定我们要处理的注解名为 ViewAutoLoad
,定义为:
@Retention(RetentionPolicy.CLASS) //保留此注解到编译期 @Target(ElementType.FIELD) //此注解只适用于“字段” public @interface ViewAutoLoad { }
本文通过介绍对字段注解的处理来讲述如何实现注解预处理,对于方法,用法其实没啥区别。
然后,重写 process
方法:
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return true; }
为啥要留个 return true
? true表示这个注解已经被我们处理过了,编译器不用再调用其他注解处理器了。
然后开始写我们的处理代码,这里就有两种处理注解的办法了:
办法1:一次性全局处理注解
这种方法 不能 知道这个字段(方法)到底是哪个类的,自然也不能获取除了你正在处理的字段(方法)所在类的其他信息,但是用起来方便一些。
获取 全局所有
具有此注解的字段,然后用 processAnnotation
方法逐一处理它们:
roundEnv.getElementsAnnotatedWith(ViewAutoLoad.class).forEach(this::processAnnotation);
这里先讲一些常用操作,假定我们现在在实现上文的 processAnnotation
方法,它的方法签名为:
private void processFormNotEmpty(Element annotatedElement)
获取字段的类型
如果你将来要生成代码或者将注解用作编译时检查,十有八九要用到这个字段的类型。
TypeMirror fieldType = annotatedElement.asType();
获取这个字段的注解,或者注解的值
ViewAutoLoad annotation = annotatedElement.getAnnotation(ViewAutoLoad.class);
现在,你可以直接使用你在注解接口定义的方法了,虽然作为示例的 ViewAutoLoad
没定义任何方法。
假装定义了 value()
: annotation.value()
获取这个字段(方法)的名字
我觉得这个肯定会用吧
Name fieldVarName = annotatedElement.getSimpleName(); //string: fieldVarName.toString();
获取这个方法的修饰符
annotatedElement.getModifiers()
返回一个集合,这个集合装着 javax.lang.model.element.Modifier
这个枚举
办法2:逐类处理注解
虽然麻烦了点,但是这个办法让我们可以知道我们在处理哪个类了。
我们回到 process
方法:
Set<? extends Element> rootElements = roundEnv.getRootElements();
这次我们直接拿到所有编译器处理的类的基础信息了,嗯,没有过滤器。
现在我们得手撸过滤器了,既然是 Set,先遍历走起。
然后怎么过滤呢?这里有一些思路:
-
给字段(方法)上注解的时候就指定好这个类的名称,比如
@Example("com.kenvix.test.TestClass")
注意:不要指定成TestClass.class
,在编译期无法这样读取类名,因为类尚未编译。 - 遍历所有类,通过字段(方法)的一些特征查找这个类
第一种思路
第一种可以是十分简单粗暴了。
String targetName = "com.kenvix.test.TestClass"; Element targetClass = null; for (Element element : rootElements) { if(element.toString().equals(targetName)) { targetClass = element; break; } } //这里只拿到了类,注解处理方法暂时省略,见下文。
第二种思路
显然,第一种实在是不怎么优雅,第二种方法又有这些思路:
android.*
Map<Element, List<Element>> tasks = new HashMap<>(); for (Element classElement : rootElements) { if(classElement.toString().startsWith(Environment.TargetAppPackage)) { List<? extends Element> enclosedElements = classElement.getEnclosedElements(); for(Element enclosedElement : enclosedElements) { List<? extends AnnotationMirror> annotationMirrors = enclosedElement.getAnnotationMirrors(); for (AnnotationMirror annotationMirror : annotationMirrors) { if(ViewAutoLoad.class.getName().equals(annotationMirror.getAnnotationType().toString())) { //好像没有其他办法在这里判断是否是目标注解了 if(!tasks.containsKey(classElement)) tasks.put(classElement, new LinkedList<>()); tasks.get(classElement).add(enclosedElement); } } } } }
这样,这个 Map<> 中就包含了我们需要的类和这个类持有的字段了,接下来进行处理即可
嗯?效率低?这是编译期,加钱换CPU或用第一种,请(手动滑稽)
生成代码
这里需要用到 javapoet 这个依赖,编辑gradle配置,加入依赖:
implementation 'com.squareup:javapoet:1.8.0'
然后重写 init 方法:
protected Types typeUtil; protected Elements elementUtil; protected Filer filer; protected Messager messager; protected ProcessingEnvironment processingEnv; @Override public synchronized final void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.processingEnv = processingEnv; typeUtil = processingEnv.getTypeUtils(); elementUtil = processingEnv.getElementUtils(); filer = processingEnv.getFiler(); messager = processingEnv.getMessager(); onPreprocessorInit(); messager.printMessage(Diagnostic.Kind.NOTE, "Preprocessor: " + this.getClass().getSimpleName() + " Initialized"); }
回到 process 方法,刚才我们已经拿到了要处理的注解,接下来开始处理这些注解:
JavaPoet 资料到处都是啊,要写还不容易?
我咋取一个不可能导入的包的类型?
这问题还是很常见的,比如我们没法在一个 Java 项目中用 Android 包的东西,但是却需要生成相关的代码.
例如,我们需要用到一个类 AppCompatActivity,它在 android.support.v7.app
这个包,则可以这样写:
ClassName appCompatClass = ClassName.get("android.support.v7.app", "AppCompatActivity");
我咋表示类型通配符、泛型限定?
接上,我们还想表示 ? extends AppCompatActivity
,可以这样写:
MethodSpec.Builder builder = code; //这里是你的方法builder builder.addTypeVariable(TypeVariableName.get("T", appCompatClass)).addParameter(TypeVariableName.get("T"), "target")
保存我们的生成的代码,并在下一步编译生成的代码
回到 process 方法,加上:
if(roundEnv.processingOver()) { //创建FormChecker这个类 TypeSpec formChecker = TypeSpec.classBuilder("FormChecker") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethods(methods) .build(); //创建类文件 JavaFile javaFile = JavaFile.builder("com.kenvix.eg.generated", formChecker) .addFileComment(getFileHeader()) .build(); try { javaFile.writeTo(filer); } catch (IOException ex) { throw new IllegalStateException(ex.toString()); } }
对同一个 javaFile
, javaFile.writeTo(filer)
只能调用一次,故需要判断是否为最后一轮注解预处理。
其他的可以看看 这篇文章 ,虽然标题挺扯的(够你:horse:)
其他小问题
我咋调试啊
显然这个时候按 IDE 的断点按钮是莫得了。
直接 System.out
或 Logger
也不太好,分分钟被一堆垃圾编译消息淹没。用着还麻烦。
好吧,其实有个简单粗暴的方法,抛个运行时异常嘛,这样就能直接停止编译然后让 IDE 显示我们想要的东西了。
throw new IllegalStateException("something");
IDEA 对 addModifiers(), javaFile.writeTo(filer) 报错
IDEA bug
别理他,编译就行了
以上所述就是小编给大家介绍的《Java 注解预处理 Annotation Processing & 代码生成》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Masterminds of Programming
Federico Biancuzzi、Chromatic / O'Reilly Media / 2009-03-27 / USD 39.99
Description Masterminds of Programming features exclusive interviews with the creators of several historic and highly influential programming languages. Think along with Adin D. Falkoff (APL), Jame......一起来看看 《Masterminds of Programming》 这本书的介绍吧!