关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。
本文章同时对一般 Java 项目和 Android 项目适用。
诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太 慢 了。
啥?反射不慢?来来来,一个 Activity 就用几十次反射,要不要和复制粘贴做一下对比?(手动阴险)
那反射这么慢,有没有什么办法?当然就是今天的主题了——代码生成: 让编译器来给你“复制粘贴”,既优雅,又高效(反正生成的代码你也不看)。
关于注解预处理的基本使用方法的资料还是很多的,这里就不细说了,概括一下就是:
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表示这个注解已经被我们处理过了,编译器不用再调用其他注解处理器了。
然后开始写我们的处理代码,这里就有两种处理注解的办法了:
这种方法 不能 知道这个字段(方法)到底是哪个类的,自然也不能获取除了你正在处理的字段(方法)所在类的其他信息,但是用起来方便一些。
获取 全局所有
具有此注解的字段,然后用 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
这个枚举
虽然麻烦了点,但是这个办法让我们可以知道我们在处理哪个类了。
我们回到 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 bug
别理他,编译就行了