EzraIO/thrift-annotation-lint
GitHub: EzraIO/thrift-annotation-lint
一个零运行时依赖的 Java 编译期注解处理器,用于检测使用 Thrift 注解的模型中的字段冲突、类型不兼容、union 安全隐患等元数据问题。
Stars: 1 | Forks: 0
# ThriftAnnotationLint
[](https://github.com/EzraIO/thrift-annotation-lint/actions/workflows/ci.yml)
[](https://github.com/EzraIO/thrift-annotation-lint/releases)
[](https://central.sonatype.com/artifact/io.github.ezraio/thrift-annotation-lint)
[](#兼容性)
[](LICENSE)
ThriftAnnotationLint 是一个零运行时依赖的 Java 注解处理器。它能够发现重复的字段 ID、无效的构造函数和 union、不兼容的 Java 类型以及未声明的递归模型,同时 `javac` 依然能够直接指向引发问题的源元素。
- **尽早发现失败:** 将元数据和 codec 初始化失败转化为编译器诊断信息。
- **安全采用:** 在启用破坏构建的 `strict` 模式之前,先使用 `warning` 模式审核现有的模型库。
- **保持构建精简:** 该处理器使用标准的 JSR 269 API,并且不会增加任何应用程序运行时依赖。
- **支持多种方言:** 已针对 Facebook Swift 和 Airlift Drift 进行了验证。
## 在源码处发现失败
这个模型作为普通的 Java 代码可以编译通过,但是有两个逻辑字段重用了同一个 Thrift 字段 ID:
```
@ThriftStruct
public class DuplicateIds {
@ThriftField(7)
public String first;
@ThriftField(7)
public String second;
}
```
普通的 Java 编译会接受这个类。如果没有更早的检查,这种失败可能只有在 Swift 或 Drift 构建运行时元数据或 codec 时才会显现出来。ThriftAnnotationLint 会在第二个注解处报告该问题:
```
error: [AW2002] Thrift model 'example.DuplicateIds' uses field ID 7
for different logical fields [first, second].
```
它还会捕获缺失的读/写路径、无效的构造函数和 builder、不安全的 union 定义、不兼容的 Java 类型、未声明的递归边界,以及通过源码或 classpath 引用触及的无效的精确泛型模型。
## 快速开始
以下坐标在 `0.2.0` 发布到 Maven Central 后即可使用。对于现有的代码库,请从 `warning` 模式开始,在审查发现的问题之后再切换到 `strict` 模式。
### Maven
```
com.facebook.swift
swift-annotations
0.23.1
org.apache.maven.plugins
maven-compiler-plugin
3.11.0
io.github.ezraio
thrift-annotation-lint
0.2.0
-Athrift.annotation.lint.mode=strict
```
### Gradle
```
repositories {
mavenCentral()
}
dependencies {
// Use this for Facebook Swift models:
compileOnly "com.facebook.swift:swift-annotations:0.23.1"
// Or this for Airlift Drift models:
// compileOnly "io.airlift.drift:drift-api:1.18"
annotationProcessor "io.github.ezraio:thrift-annotation-lint:0.2.0"
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += ["-Athrift.annotation.lint.mode=strict"]
}
```
JAR 会通过标准的注解处理器服务文件自动注册,并声明 Gradle 聚合行为,因此不需要显式指定 `-processor` 类名。
可运行的示例目录可在
[`examples/maven`](examples/maven/README.md) 中找到。
## 它能捕获的问题
- 冲突的字段 ID、名称、requiredness 以及 IDL 注解;
- 缺失的提取或注入路径,以及反射顺序的歧义;
- 无效的构造函数、builder、setter 签名以及成员修饰符;
- 不受支持或不兼容的 Java 类型和嵌套容器类型;
- 未显式声明为递归的递归模型循环;
- 不安全的 union discriminator、构造路径和 payload ID;
- 无效的 enum 值方法,多个 Drift unknown-enum fallback,以及不收敛的精确泛型模型图。
## 支持范围
ThriftAnnotationLint 会扫描以下模型注解:
- `@ThriftStruct`
- `@ThriftUnion`
- `@ThriftEnum`
- `@ThriftField`
- `@ThriftConstructor`
- `@ThriftUnionId`
- `@ThriftEnumValue`
- Drift `@ThriftEnumUnknownValue`
该处理器在验证 ID、名称、requiredness、读/写路径、签名以及递归嵌套的 Java 类型之前,会从 Java 字段、getter、setter、构造函数参数和 builder 方法中构建逻辑字段。
当应用程序编译器支持 Java record,并且该 record 暴露了带注解的构造函数和访问器路径时,它们也会受到支持。
只有当 Lombok 生成的方法确实带有 `@ThriftField` 并且在注解处理期间对 ThriftAnnotationLint 可见时,Lombok 生成的成员才会被处理。
普通的 `@Data`、`@Getter` 或 `@Setter` 不会创建 Swift 元数据,因此 ThriftAnnotationLint 不会从这些注解中推测注入路径。如果显式列出了处理器,请在 ThriftAnnotationLint 之前运行 Lombok,并为生成的模型保留运行时 codec 测试。
预览版**不会**验证:
- Thrift IDL 文件或 IDL 编译器生成输出的正确性
- RPC service 注解
- 不同版本之间的 schema 兼容性
- 需要执行用户代码才能确定的值
- 仅用于 service 的类型,例如模型字段上的 `ListenableFuture`
这种版本间的边界是刻意为之。真实的 Thrift 用户遇到的问题通常涉及废弃和重用字段 ID、更改字段类型,或者弱化已发布的 `required` 字段。这些检查需要旧的 schema/模型基准,而不仅仅是当前的 JSR 269 编译。该处理器捕获当前构建中不安全的元数据;未来的兼容性模式可能会单独比较已提交的基准。
有关诊断分类和已知的运行时限制,请参阅[规则参考](docs/rules.md)。
## 相比官方 Swift 的安全性扩展
Swift 元数据可以表示一些虽然构造合法,但对于对称的生产环境 codec 而言并不安全的结构。ThriftAnnotationLint 会刻意拒绝这些结构:
- 只读或只写的逻辑字段;每个字段必须同时具备提取和注入路径;
- 不同的逻辑字段共享同一个 ID,尽管 Swift 可以根据 ID 合并成员并选择一个名称;
- 被用作直接注入目标的 final 字段;
- 抽象或非静态的成员构造类型,以及抽象的 builder;
- 不是可写的原始类型 `short` 值的 union ID 成员,因为默认的编译器 codec 不会对其 discriminator 路径进行装箱或拆箱操作;
- 没有确定性的无参构造函数或基于变体的构造路径的 union;
- union payload 字段 ID `0`,这会与默认编译器 codec 初始的无字段 discriminator 发生冲突;
- 具体的 Java 形状无法安全地分配给 Swift 规范的 `List`、`Set`、`Map` 或 `ByteBuffer` codec 形状的容器提取/注入路径,包括不安全的嵌套具体容器;
- 多个 getter 提取路径;在没有 getter 确定性地替换它们的情况下存在多个字段提取路径;或者针对一个逻辑字段存在多个 union 方法注入路径,因为官方元数据在未指定反射顺序的情况下,在胜出层级中仅保留其中一个;
- 没有边被标记为 `isRecursive=TRUE` 的直接递归模型循环;
- Swift 可能会根据迭代顺序进行解析的冲突或重复的元数据。
这些是产品安全规则,并不意味着官方运行时会拒绝所有这类结构。在首次应用于现有代码库时,请使用 `-Athrift.annotation.lint.mode=warning`。
## 处理器模式
`thrift.annotation.lint.mode` 接受两个区分大小写的值:
| 值 | 行为 |
| --- | --- |
| `strict` | 默认值。模型违规将被视为编译错误。 |
| `warning` | 模型违规将被视为警告,以便在强制执行之前对现有代码进行审计。 |
无效的选项和内部处理器失败始终会被视为编译错误。预览版有意不提供按规则抑制的机制。
`thrift.annotation.lint.maxExactModels` 是一个正整数,默认值为 `512`。它限制了从源码根节点可访问的额外精确泛型模型实例;源码根本身不消耗此预算。只有在身份被完全解析并确认为模型后才会扣除额度,因此临时生成的类型和随后被归类为容器的声明不会产生错误的 `AW9003` 诊断。超出预算将报告 `AW9003`,而不是允许出现分支或不收敛的元数据图耗尽编译器。仅在审查过且有限的图结构下才应提高此值。
## 兼容性
| 组件 | 支持的基准 |
| --- | --- |
| 处理器字节码 | Java 8 |
| 用于编译应用程序的 JDK | 8, 11, 17, 21 |
| Facebook Swift | 请参阅下方已验证的发布矩阵 |
| Airlift Drift | 已验证 `1.18` 注解;请参阅下方说明 |
| Maven | 3.6.1 或更高版本 |
该处理器产物没有运行时依赖,仅使用标准的 JSR 269 编译器 API。Facebook Swift、Airlift Drift 及其传递依赖仅被该项目的测试套件所使用。
| 官方 Swift 版本 | 注解/编译器夹具 | 官方元数据和 codec 夹具 |
| --- | --- | --- |
| `0.19.2` | 已验证 | 已使用默认编译器 codec 验证 |
| `0.20.0` | 已验证 | 已使用默认编译器 codec 验证 |
| `0.21.1` | 已验证 | 已使用默认编译器 codec 验证 |
| `0.22.1` | 已验证 | 已使用默认编译器 codec 验证 |
| `0.23.1` | 已验证 | 已使用默认编译器 codec 验证 |
这五个确切的版本构成了预览版的支持矩阵;并不暗示对每一个数字居中的中间版本都兼容。矩阵之外的版本、未经验证的分支以及源码不兼容的变体不作兼容性声明。可以使用 `mvn -Dswift.version= verify` 在本地评估候选版本;测试通过是所覆盖 API 和夹具的有效证据,但并不保证适用于特定于应用程序的运行时配置。
codec 保证涵盖了 Swift 的默认 `CompilerThriftCodecFactory`。`ReflectionThriftCodecFactory` 是一种自定义的运行时配置,具有不同的 union-builder 调用语义;选择使用它的应用程序必须保留针对特定工厂的集成测试。
Airlift Drift `1.18` 是其发布的产物仍保留 Java 8 字节码的最新版本,因此是预览版已验证的 Drift 基准。较新的 Drift 版本使用相同的核心模型注解,但需要较新的 Java 运行时;它们尚未被声明为已验证。支持 Drift 的模型发现、字段、构造函数、builder、union、enum、IDL 注解以及共享的 Java 类型规则。一个模型必须一致地使用一种注解方言;混用 `com.facebook.swift.codec.*` 和 `io.airlift.drift.annotations.*` 注解会被拒绝。
Drift enum 必须暴露且仅暴露一个有效的 `@ThriftEnumValue` 方法;Swift enum 继续允许零个或一个。未加注解的 Java enum 会继承引用它的模型的方言,并在从两种方言访问时独立进行验证。显式注解的 Swift 模型不能被 Drift 模型引用(反之亦然);这会在引用处报告为 `AW1001`。
Drift 字段支持 `Optional`、`OptionalInt`、`OptionalLong` 和 `OptionalDouble`。泛型 Optional 元素会被递归检查,包括嵌套的容器和模型,并规范化为元素的传输类型(wire type)。原始的 `Optional` 和不支持的元素类型仍会报告 `AW4001`。Facebook Swift 模型不接受 Optional 类型。
## 编译时与运行时的边界
ThriftAnnotationLint 从不加载应用程序类或执行模型方法。这对于安全、确定性的构建很重要,但这意味着某些运行时事实无法被静态证明。例如,一个 `@ThriftEnumValue` 方法可能具有有效的签名,但在运行时返回重复或 null 值。对于依赖于可执行值或运行时配置的检查,Swift 运行时元数据验证仍然是最终权威。
泛型验证是按需驱动且感知使用位置的。从带注解的源模型开始,ThriftAnnotationLint 会追踪可达的模型引用,将确切的泛型具体参数替换到带注解的成员中,并验证由此产生的实例化形状,即使被引用的模型来自 classpath。不相关的 classpath 模型不会被扫描。未绑定的声明类型变量和原始泛型模型不会仅仅因为不存在具体的使用位置绑定而被拒绝。自定义的 `ThriftCatalog` 强制类型转换仍然是仅限运行时的。
官方 Swift 会在容器接口之前对 Java enum 进行分类。对于其他声明的类型,它会在检查 `@ThriftStruct` 或 `@ThriftUnion` 之前,按照 `Map`、然后是 `Set`、最后是 `Iterable` 的顺序对容器子类型进行分类。ThriftAnnotationLint 在生成类型的轮次中反映了这种优先级:一个实现了 `Iterable` 的 enum 仍然是 enum,而带注解的容器类上看起来像模型的成员会被忽略,但其解析出的元素/键/值类型以及可达的带注解模型仍会被验证。
具体的容器根节点可能会编码为其规范接口,但解码为规范集合;直接的根节点往返测试仍然是应用程序集成测试的责任。
支持矩阵中的官方 Swift 版本使用 Paranamer 的字节码读取器来获取构造函数和多参数注入的名称。因此,ThriftAnnotationLint 仅在 classpath 名称存在于 `LocalVariableTable` 中时才信任它们;有意忽略单独的 `MethodParameters` 属性。当 LVT 数据缺失时,ThriftLint 会在 Swift ID 推断的两个阶段中模拟 GeneralParanamer 确定性的 `arg0`、`arg1`... 回退机制。作为一项 codec 安全规则,参数仍必须声明显式的 `@ThriftField` ID 或名称。
为了兼容 Java 8,通过注解处理器 `CLASS_PATH` 读取 classpath 字节码。如果模型依赖项仅在命名模块路径上可用,并且参数名称无法完全由注解提供,ThriftAnnotationLint 会以 `AW3003` 直接失败,而不是猜测 LVT 缺失。请将该模型依赖项放在处理器 classpath 上,或提供完整、稳定的注解名称。
对于在当前源码轮次中编译的声明,JSR 269 会暴露源码名称,但无法证明最终的类文件将保留 LVT 数据。因此,ThriftAnnotationLint 会同时验证源码/LVT 名称和可能的无 LVT `argN` 分支,并要求在源码构造函数和多参数注入参数上显式声明 `@ThriftField` ID/名称,或提供稳定的基于注解的名称。它还反映了 ThriftFieldParanamer 的“所有参数都需注解命名”规则,包括有序的 `@ThriftField(name=...)` 和 JSR-330 `@Named` 注解。
相反,Airlift Drift `1.18` 会先检查 `@ThriftField(name=...)`,然后检查其参数名读取器(方法元数据、字节码调试名称,最后是反射回退)。为了实现确定性的构建/运行时一致性,在没有稳定的字节码名称时,Drift 注入参数应声明显式的字段 ID 或名称;处理器会拒绝那些否则将依赖于编译器调试信息或 `-parameters` 设置的身份。
注解处理器也可以在较早的轮次检查了其消费者之后,生成被引用的类型。ThriftAnnotationLint 会推迟对显式未解析结构的检查,并在随后的每一个生成源码轮次中重建所有历史源码根节点的需求闭包。这也能捕获那些保持为 `DECLARED` 而没有暴露 `ERROR` 镜像的 javac 占位符。因此,临时的生成符号间隙不会变成不可逆转的错误诊断。
ThriftAnnotationLint 旨在防止可静态检测到的元数据失败,而不是替代集成测试或运行时 codec 初始化测试。
## 预览版升级策略
这是第一个公开预览版,有意不暴露任何兼容性别名。服务发现会加载 `io.github.thriftannotationlint.ThriftAnnotationLintProcessor`;仅在需要显式 `-processor` 参数时才使用该类名。
在审计现有模型库时,请从 `-Athrift.annotation.lint.mode=warning` 开始,解决诊断问题后再切换到 `strict`。所有面向用户的消息均为英文,并使用稳定的 `AWxxxx` 规则代码。
## 构建
```
mvn verify
```
## 可运行的 Maven 演示
从源码检出版本中,运行:
```
sh examples/maven/run-demo.sh
```
该脚本会将当前检出版本安装到本地 Maven 仓库,然后编译一系列有效和故意无效的 Swift 模型。它会在警告模式下验证每一个可运行的、稳定的面向用户的规则类别,演示对重复 ID 的严格拒绝,并检查始终报错的选项和精确模型预算的防护措施。AW9002 是不可运行的内部安全网,并已在案例目录中记录。运行 `sh examples/maven/run-demo.sh --all-modes` 可以在警告和严格模式下检查每一个常规规则。有关案例目录和等效的手动命令,请参阅[示例 README](examples/maven/README.md)。
CI 会在 JDK 8、11、17 和 21 上运行默认的 `0.23.1` 套件,并在 JDK 8 上为兼容性矩阵中的每一个确切 Swift 版本运行完整套件。该项目在禁用注解处理 (`proc:none`) 的情况下编译自己的处理器,并通过测试验证打包的服务元数据。该套件还运行官方的 Swift 元数据夹具和 builder/union codec 往返测试,以及 Drift 注解/编译器兼容性夹具。
维护者在更改轮次处理、类型解析、提取、验证或诊断路由之前,应阅读[架构与行为不变性](docs/architecture.md)。
## 独立性与许可
ThriftAnnotationLint 是一个独立的项目。它不隶属于、不受认可,也不是 Apache 软件基金会、Meta、Facebook 或 Airlift 的官方组件。Facebook Swift 和 Airlift Drift 是独立的项目,仍受其各自许可证的约束。
ThriftAnnotationLint 根据 [Apache License, Version 2.0](LICENSE) 获得许可。
标签:JS文件枚举, Thrift, 云安全监控, 后台面板检测, 域名枚举, 注解处理器, 编译时检查, 静态分析