内容简介:关于 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 & 代码生成》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。