惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
云风的 BLOG
云风的 BLOG
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
J
Java Code Geeks
博客园 - 聂微东
B
Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
腾讯CDC
L
LangChain Blog
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Azure Blog
Microsoft Azure Blog
D
DataBreaches.Net
The GitHub Blog
The GitHub Blog
美团技术团队
博客园 - Franky
Google DeepMind News
Google DeepMind News
V
V2EX
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
The Cloudflare Blog

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog 利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog 状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 Kotlin 的那些骚操作 | Kenvix's Blog 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 扔鸡蛋问题 | Kenvix's Blog 计算机幻觉从入门到入土 | Kenvix's Blog Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog
Java 注解预处理 Annotation Processing & 代码生成
2019-02-02 · via Kenvix's Blog

关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。

本文章同时对一般 Java 项目和 Android 项目适用。

为何使用 Java 注解预处理

诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太了。

啥?反射不慢?来来来,一个 Activity 就用几十次反射,要不要和复制粘贴做一下对比?(手动阴险)

那反射这么慢,有没有什么办法?当然就是今天的主题了——代码生成: 让编译器来给你“复制粘贴”,既优雅,又高效(反正生成的代码你也不看)。

如何使用 Java 注解预处理

关于注解预处理的基本使用方法的资料还是很多的,这里就不细说了,概括一下就是:

  1. 建一个类,继承并实现 javax.annotation.processing.AbstractProcessor
  2. 建一个 META-INF.services.javax.annotation.processing.Processor
  3. 在这里写上你的预处理器的完整类名(带包名)

注意:对于 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 项目中,包名符合 android.* 就可以去掉了。或者只看包名以自己项目开头的。
  • 字段(方法)的名称有一些我们可以利用的特征,例如都以类名开头
  • 一个类中所有字段(方法)的数量和名称是唯一的。也就是说,不存在两个类同时有一样数量的同名字段。
  • 找到有目标注解的类后,将这个类和目标注解直接加到 Map
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());
    }    
}

对同一个 javaFilejavaFile.writeTo(filer) 只能调用一次,故需要判断是否为最后一轮注解预处理。

其他的可以看看这篇文章,虽然标题挺扯的(够你🐴)

其他小问题

我咋调试啊

显然这个时候按 IDE 的断点按钮是莫得了。
直接 System.outLogger 也不太好,分分钟被一堆垃圾编译消息淹没。用着还麻烦。
好吧,其实有个简单粗暴的方法,抛个运行时异常嘛,这样就能直接停止编译然后让 IDE 显示我们想要的东西了。

throw new IllegalStateException("something");

IDEA 对 addModifiers(), javaFile.writeTo(filer) 报错

IDEA bug
别理他,编译就行了

想用 getClass() 反射处理?

别想了,类都没编译好呢你get个啥