模块:io.github.microsphere-projects:microsphere-java-annotations · 0.3.20-SNAPSHOT(release 分支)
包:io.microsphere.annotation(6 个类型)· io.microsphere.annotation.concurrent(2 个类型)—— 8 个注解接口、
6 个测试类,再无其他代码 · 整个 reactor 中不存在任何 maven.compiler.* / <release> 属性,因此字节码级别继承自
外部父工程 microsphere-build:0.3.16(pom.xml:6-10);CI 跑在 JDK 8 / 11 / 17 / 21 / 25
(.github/workflows/maven-build.yml:25、:33)。
下文使用的路径前缀(均相对于模块根目录,引用前都逐行打开确认过):
annotation/…=microsphere-java-annotations/src/main/java/io/microsphere/annotation/…concurrent/…=microsphere-java-annotations/src/main/java/io/microsphere/annotation/concurrent/…- 本模块测试位于
src/test/java/io/microsphere/annotation/… - 其他模块一律带完整模块前缀引用,例如
microsphere-java-core/src/main/java/io/microsphere/…
它是契约层(contract layer):一个叶子构件,其他每个模块都可以用它来加注解,而它不依赖 reactor 里的任何东西。
它唯一的编译依赖是 com.google.code.findbugs:jsr305,声明为 optional
(microsphere-java-annotations/pom.xml:24-28);除此之外只有 test 作用域的 JUnit 5
(microsphere-java-annotations/pom.xml:30-41)。
graph TD
ANN["microsphere-java-annotations(叶子)<br/>8 个注解类型 · jsr305 optional"]
CORE["microsphere-java-core"]
JDK["microsphere-jdk-tools"]
LM["microsphere-lang-model"]
AP["microsphere-annotation-processor"]
JT["microsphere-java-test"]
AT["microsphere-annotation-test"]
DEP["microsphere-java-dependencies(BOM,管理版本)"]
ANN -->|"compile,非 optional<br/>core/pom.xml:24-28"| CORE
ANN -->|"compile,optional<br/>java-test/pom.xml:24-29"| JT
CORE --> JDK
CORE --> LM
CORE --> AP
LM --> AP
AT --> JT
AT --> JDK
AP -.->|"test 作用域<br/>processor/pom.xml:51-56"| AT
CORE -.->|"test 作用域<br/>core/pom.xml:65-71"| JT
AP --> META["META-INF/microsphere/<br/>configuration-properties.json"]
DEP -.->|管理版本| ANN
DEP -.->|管理版本| CORE
两条入向事实,都是验证得来而非假设:
core确实在 compile 作用域依赖本模块,并且没有标 optional (microsphere-java-core/pom.xml:24-28)。因此 core 发布的每一个注解(@Nonnull、@Nullable、@Immutable、@ConfigurationProperty)都经由本构件解析,消费者会传递性地拿到它们。core 只在 test 作用域声明jsr305(microsphere-java-core/pom.xml:73-78),所以javax.annotation.*是被有意排除在 core 的运行期契约之外的。- 处理器是按名字、而不是按类标识触达注解的。
CONFIGURATION_PROPERTY_ANNOTATION_CLASS_NAME = "io.microsphere.annotation.ConfigurationProperty"(microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ConfigurationPropertyJSONElementVisitor.java:59) 就是喂给@SupportedAnnotationTypes的值 (.../ConfigurationPropertyAnnotationProcessor.java:77),也是驱动 mirror 查找的值 (.../ConfigurationPropertyJSONElementVisitor.java:71)。整个过程从不要求Class对象在场。
已验证的消费方普查(整个 release worktree 的主源码,按 import 语句计数):
| 注解 | 导入它的主源码文件 | 生产环境反射读取方 | 编译期读取方 |
|---|---|---|---|
@Nonnull |
99 | 无(仅测试,src/test/java/io/microsphere/annotation/NonnullTest.java:36) |
无 —— 面向分析器 |
@Nullable |
76 | 无(仅测试,…/NullableTest.java:36) |
无 |
@Immutable |
74 | 无(仅测试,…/ImmutableTest.java:36) |
无 |
@ConfigurationProperty |
14 个 core/工具文件,2 个测试支撑文件 | microsphere-java-test/src/test/java/io/microsphere/test/model/ConfigurationPropertyModelTest.java:84-85 |
microsphere-annotation-processor(按名字) |
@Since |
2(microsphere-java-test/src/main/java/io/microsphere/test/annotation/TestAnnotation.java:69、…/service/TestServiceImpl.java:70) |
生产环境无;在 microsphere-java-core/src/test/java/io/microsphere/util/AnnotationUtilsTest.java:469 作为夹具使用 |
无 |
@Experimental |
0 | 0 —— assertNull 证明了不可见(…/ExperimentalTest.java:35) |
无 |
@ThreadSafe / @NotThreadSafe |
0 | 0(不可能有:CLASS retention) |
0 |
Important
从下往上读这张普查表:八个注解里有五个在仓库中没有任何生产环境的反射读取方。唯一跨模块、读取注解类对象的
反射消费方是 ConfigurationPropertyModelTest(:84-85);其余代码要么在打注解,要么在读生成的 JSON 资源。
这不是意外——这正是“契约层”的定义:库声明承诺,工具据此行动。
| 类型 | @Target |
@Retention |
@Documented |
属性 | JSR-305 关联 | 证据 |
|---|---|---|---|---|---|---|
annotation/Nonnull.java |
缺失 → 所有声明上下文,不含 type-use | RUNTIME(:33) |
是(:32) |
无 | @javax.annotation.Nonnull(:34)+ @TypeQualifierNickname(:35) |
:32-36 |
annotation/Nullable.java |
缺失(同上) | RUNTIME(:35) |
是(:34) |
无 | @javax.annotation.Nonnull(when = MAYBE)(:36)+ @TypeQualifierNickname(:37) |
:34-38、:24 |
annotation/Immutable.java |
缺失(同上) | RUNTIME(:32) |
是(:31) |
无 | 无 | :31-34 |
annotation/Since.java |
全部 10 个 element type(:43-54) |
RUNTIME(:42) |
是(:55) |
module() ""(:61)、value() 必填(:66) |
无 | :42-56 |
annotation/Experimental.java |
ANNOTATION_TYPE, CONSTRUCTOR, FIELD, METHOD, TYPE(:38-44) |
SOURCE(:37) |
是(:36) |
description() ""(:50) |
无 | :36-50 |
annotation/ConfigurationProperty.java |
FIELD(:37) |
RUNTIME(:36) |
是(:35) |
name ""(:60)、type String.class(:67)、defaultValue ""(:74)、required false(:81)、description ""(:88)、source {}(:95) |
无 | :35-95 |
concurrent/ThreadSafe.java |
TYPE(:27) |
CLASS(:28) |
是(:26) |
无 | 未应用(:9 处 import 未被使用) |
:26-29 |
concurrent/NotThreadSafe.java |
TYPE(:26) |
CLASS(:27) |
是(:25) |
无 | 未应用(:9 处 import 未被使用) |
:25-28 |
三组划分解释了 retention 的分布,而每一组都是有意的(尽管未被文档化的)决定:
SOURCE×1 ——@Experimental是在使用点给作者看的警告;一旦编译完成就再没有东西可强制执行,因此为它付出 运行期属性的代价既不花钱也不赚到什么。CLASS×2 —— JCIP 那一对,指向字节码分析器;改成RUNTIME只会撑大常量池。RUNTIME×5 ——@Nonnull/@Nullable/@Immutable可被反射式工具读取 (io.microsphere.util.AnnotationUtils),@Since携带内省工具需要的数据,而@ConfigurationProperty必须同时 可被javax.lang.model访问者以及反射读取。
annotation/Nonnull.java:32-36 和 annotation/Nullable.java:34-38 声明了 @Documented、
@Retention(RUNTIME)、JSR-305 的 qualifier 和 @TypeQualifierNickname —— 但没有 @Target。按
JLS §9.6.4.1,没有 @Target 的注解类型适用于每一个声明上下文(type、field、method、parameter、constructor、
local variable、package、annotation type),而不适用于任何 type-use 上下文。按重要性排序的实际后果:
| 放置位置 | 合法? | 原因 |
|---|---|---|
@Nonnull public static Foo get() |
是 | 方法声明 |
void f(@Nonnull String s) |
是 | 参数声明 |
@Nonnull class X {} |
是 | 类型声明 —— 测试就是这么做的(src/test/java/io/microsphere/annotation/NonnullTest.java:31) |
用 @Nonnull 标注 package-info.java |
是 | 没有 @Target ⇒ PACKAGE 被允许 |
List<@Nonnull String> |
否 | 需要 ElementType.TYPE_USE;只有 @Since 声明了它(annotation/Since.java:53) |
@Nonnull 用于 record 分量的类型 |
否 | 同一原因 |
典型的 JSR-305 项目会用一个包级别的
@TypeQualifierDefault(ElementType.METHOD) @Nonnull 放在 package-info.java 里,来选择“除非明说,否则一切都非空”。
在 release 上验证其缺席:
@TypeQualifierDefault在 worktree 中任何 Java 源文件里都不出现(仅有的命中来自文档正文以及.understand-anything/下的分析缓存)。- 本模块不提供
package-info.java;reactor 中唯一的一个是microsphere-java-core/src/main/java/io/microsphere/json/package-info.java。
因此 @Nonnull 的作用范围恰好就是有人标注过的那些元素,逐元素计。日后加入包级默认会悄悄改变 99 个文件的含义,
这正是应把它当作有意限制、而非疏漏的主要理由。
sequenceDiagram
participant SRC as 你的源码
participant CL as 编译后的 .class(RUNTIME 可见)
participant REF as Java 反射
participant AN as 支持 JSR-305 的分析器
participant JP as classpath 上的 jsr305
SRC->>CL: @Nonnull (io.microsphere) 保留 :33 RUNTIME
CL->>REF: element.getAnnotation(Nonnull.class) → 实例
Note over REF: 无需任何额外 jar —— Nonnull 自身字节码不依赖 javax.annotation
CL->>AN: 注解类型自带 @javax.annotation.Nonnull + @TypeQualifierNickname (:34-35)
JP-->>AN: 解析 javax.annotation.Nonnull / meta.When
AN->>AN: 按 ALWAYS qualifier 处理;Nullable 映射为 when=MAYBE (:36)
Note over JP,AN: jsr305 是 optional(pom.xml:24-28)→ 缺少它时别名无法解析,只有标记本身存活
机制上:@TypeQualifierNickname(在 annotation/Nonnull.java:19、annotation/Nullable.java:19 导入)告诉合规的
分析器*“要应用的 qualifier 就是加在我身上的那一个”*。于是两个 microsphere 注解都别名到同一个 JSR-305 类型,
仅由它的 when 参数区分 —— @Nonnull 继承默认值(ALWAYS),@Nullable 通过 annotation/Nullable.java:24 的
静态 import 设置 when = MAYBE。选择 MAYBE 而不是 javax.annotation.Nullable,让数据流格(lattice)里只保留
一个 qualifier 类型,使“可空”成为同一个谓词的一个属性,而不是第二个与之竞争的谓词。
Warning
由于 jsr305 是 optional,这些元注解对消费者不是传递可用的
(microsphere-java-annotations/pom.xml:27),而 core 进一步把它收窄为 test 作用域
(microsphere-java-core/pom.xml:73-78)。反射地看,
Nonnull.class.getAnnotation(javax.annotation.Nonnull.class) 只有在运行期 classpath 上有 jsr305 时才有答案。
本模块自己的六个测试中没有任何一个断言别名可以解析 —— 见 §8。
annotation/Nonnull.java:1-15 和 annotation/Nullable.java:1-15 携带 Spring Framework 的版权声明块
(Copyright 2002-2019 the original author or authors),而 ConfigurationProperty、Experimental、Immutable
和 Since 携带 ASF 声明块,concurrent/ThreadSafe.java:1-6 / concurrent/NotThreadSafe.java:1-6 则携带 JCIP
署名(Copyright (c) 2005 Brian Goetz、Official home: http://www.jcip.net)。命名也一脉相承:
Nonnull(第二个字母小写)对齐的是 javax.annotation.Nonnull,而不是 Spring 的 NonNull
(annotation/Nonnull.java:36)。
concurrent/ThreadSafe.java 与 concurrent/NotThreadSafe.java 几乎互为镜像:
| 方面 | ThreadSafe |
NotThreadSafe |
|---|---|---|
| javadoc 正文 | :18-22(逐字引用 JCIP 定义) |
:18-21 |
@Documented |
:26 |
:25 |
@Target(TYPE) |
:27 |
:26 |
@Retention(CLASS) |
:28 |
:27 |
交叉 @see |
:24 |
:23 |
@author / @since javadoc 标签 |
缺失 | 缺失 |
三条对维护者重要的观察:
- JSR-305 的 import 既是死代码又交叉接错。
ThreadSafe.java:9导入javax.annotation.concurrent.NotThreadSafe,NotThreadSafe.java:9导入javax.annotation.concurrent.ThreadSafe。两者都没有被应用 —— 类型上并没有@javax.annotation.concurrent.ThreadSafe这个元注解 —— 所以这一对没有任何昵称别名,与@Nonnull/@Nullable不同。跑一次 SpotBugs/FindBugs 不会把io.microsphere.annotation.concurrent.ThreadSafe读成它所命名的那个 JCIP/JSR-305 声明。要么删掉 import, 要么补上元注解以及@TypeQualifierNickname式的 linkage;当前状态是两边最差的部分都占了。 - 两个注解在
release中都没有被任何地方使用。 在整个 worktree 搜索io.microsphere.annotation.concurrent,结果只有声明它自己的那两个文件。core 是用@Immutable和结构来文档化 安全性的 ——@Immutable直接标注在两个快照字段上 (microsphere-java-core/src/main/java/io/microsphere/util/ClassPathUtils.java:60-61、:63-64),标注在由ConcurrentHashMap支撑的工具所返回的缓存上(…/util/ClassLoaderUtils.java:92、…/util/ServiceLoaderUtils.java:140),也标注在不可修改的发布物本身上 (…/util/ServiceLoaderUtils.java:468-469)。 @Immutable放错了包,就这一对的搭配而言。javax.annotation.concurrent同时容纳Immutable和线程安全 注解对;Microsphere 把它们拆开了:Immutable在io.microsphere.annotation(annotation/Immutable.java:17),注解对则在io.microsphere.annotation.concurrent(concurrent/ThreadSafe.java:7)。后果是“状态看不到变化”这个承诺(annotation/Immutable.java:24-25)和 “没有非法交错”这个承诺(concurrent/ThreadSafe.java:18-22)由两个互不相关、retention 也不同(RUNTIMEvsCLASS)的类型来标注,而且两者都不关联 JSR-305。
annotation/ConfigurationProperty.java:55-95 声明了六个属性,外加三个 String 常量(:43、:48、:53),
后者构成 source() 的封闭词汇表。javadoc 把预期目标描述为“作为配置属性的那个 Java 字段,其修饰符通常是
static 和 final”(annotation/ConfigurationProperty.java:27-28),并指向生成器模块(“名为
microsphere-annotation-processor 的模块会在 classpath 中生成元数据资源”,:30)。
ConfigurationProperty.class 的每一个读取方(对全 worktree 的 grep,列出所有命中):
| 读取方 | 种类 | 证据 |
|---|---|---|
ConfigurationPropertyModelTest |
对 getDeclaredFields() 做运行期反射 |
microsphere-java-test/src/test/java/io/microsphere/test/model/ConfigurationPropertyModelTest.java:84(isAnnotationPresent)、:85(getAnnotation) |
ConfigurationPropertyJSONElementVisitorTest |
把注解编进一次测试编译 | microsphere-annotation-processor/src/test/java/io/microsphere/annotation/processor/ConfigurationPropertyJSONElementVisitorTest.java:54,名字断言 :59 |
ConfigurationPropertyAnnotationProcessor |
@SupportedAnnotationTypes + import |
.../processor/ConfigurationPropertyAnnotationProcessor.java:20、:77 |
ConfigurationPropertyJSONElementVisitor |
按名字的 mirror 查找、六属性映射 | .../processor/ConfigurationPropertyJSONElementVisitor.java:59、:71、:79-96 |
TestAnnotation / TestServiceImpl |
注解类型的属性载体(ConfigurationProperty[] properties()) |
microsphere-java-test/src/main/java/io/microsphere/test/annotation/TestAnnotation.java:71、…/service/TestServiceImpl.java:72-74 |
io.microsphere.metadata.* |
不读注解 —— 读生成的 JSON,并建模为 io.microsphere.beans.ConfigurationProperty |
microsphere-java-core/src/main/java/io/microsphere/metadata/ConfigurationPropertyLoader.java:44(仅 @see)、…/metadata/ConfigurationPropertyReader.java:38 |
flowchart TD
A["被标注的字段:<br/>@ConfigurationProperty 位于 public static final 上<br/>例如 beans/BeanUtils.java:118-124"] --> P["javac 处理轮次<br/>ConfigurationPropertyAnnotationProcessor.process (:98-105)"]
P -->|"processingOver()"| W["writeMetadata() (:149-156)"]
P -->|"普通轮次"| R["resolveMetadata(elements) (:112-132)"]
R --> ACC["element.accept(jsonElementVisitor, jsonBuilder) (:120)"]
ACC --> SUP["JSONElementVisitor 的 supportsType<br/>lang-model:89 · 处理器覆写后返回 true :110-112"]
SUP --> VF["visitVariableAsField (:69-107)"]
VF --> MIR["getAnnotation(field, 按名字的 FQCN) (:71)<br/>FQCN 常量 :59"]
MIR --> EV["getElementValues(annotation) (:74)<br/>lang-model AnnotationUtils.java:1509-1529(声明顺序,WITH_DEFAULT)"]
EV --> MAP["属性 switch (:79-96):<br/>name → new ConfigurationProperty(name)<br/>type/defaultValue/required/description/source"]
MAP --> GEN["generator.generate(property) (:101)<br/>SPI:loadFirstService(ConfigurationPropertyGenerator) (:65)"]
GEN --> BUF["jsonBuilder.append(json).append(',')"]
BUF --> SPI["appendGeneratedConfigurationPropertyJSON (:134-140)<br/>ConfigurationPropertyLoader.loadAll()<br/>core metadata/ConfigurationPropertyLoader.java:91-106"]
SPI --> W
W --> OUT["META-INF/microsphere/configuration-properties.json<br/>ResourceConstants.java:43、:54"]
OUT --> RT["运行期读取方:<br/>ClassPathResourceConfigurationPropertyLoader.java:47、:71-77、:80-86<br/>→ ConfigurationPropertyReader.java:66、:94、:119"]
访问者把“作者省略了属性”当作“去推断它”,经由
matchesDefaultAttributeValue
(microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/AnnotationUtils.java:1145):
| 属性 | 声明的默认值 | 省略时处理器实际写出的内容 | 证据 |
|---|---|---|---|
name() |
""(annotation/ConfigurationProperty.java:60) |
field.getConstantValue() —— 字段自己的字面量值;非常量字段为 null |
.../ConfigurationPropertyJSONElementVisitor.java:119-122 |
type() |
String.class(:67) |
field.asType() —— 字段声明的 Java 类型 |
:124-128 |
description() |
""(:88) |
elements.getDocComment(field) —— 字段的 javadoc |
:130-136 |
type() 这一行是文档与实际的分歧:注解 javadoc 承诺 String,然而为 microsphere.io.buffer.size 生成的元数据是
"type": "int"
(microsphere-java-core/src/main/resources/META-INF/microsphere/configuration-properties.json:32),与
microsphere-java-core/src/main/java/io/microsphere/io/IOUtils.java:85-89 处被标注的 int 字段一致。要改的是
javadoc,不是访问者。
visitVariableAsField 只在 name 分支里创建模型(:79-81),却在之后每个分支里解引用它
(:84、:87、:90、:93、:95)—— 于是它隐含要求 name() 最先被访问。今天这条成立,是因为
getElementValues 按声明顺序迭代 getDeclaredMethods(annotationType)
(microsphere-lang-model/.../AnnotationUtils.java:1514、:1518-1527),而 name() 就是第一个声明的属性
(annotation/ConfigurationProperty.java:60)。调整注解接口里属性的顺序会让处理器抛出
NullPointerException —— 两个文件里都没有任何地方记录这个约束。要么显式先迭代 name,要么把构造移出 switch。
剩下的步骤是机械的但值得知道:sources 是在 else 分支里追加到 Metadata.getSources() 的(:95,实现在
:143-152),declaredClass / declaredField 总是用字段自身覆写(:154-163),数组的中括号是靠替换结尾逗号
拼出来的(:126-131),而 JSON 被 JSONArray 重新解析一遍纯粹是为了按缩进 2 美化输出(:151-152)。
处理器通过 META-INF/services/javax.annotation.processing.Processor:1 注册自己
(microsphere-annotation-processor/src/main/resources/)。输出格式可插拔
(ConfigurationPropertyGenerator,默认实现 DefaultConfigurationPropertyGenerator,注册于
microsphere-java-core/src/main/resources/META-INF/services/io.microsphere.metadata.ConfigurationPropertyGenerator:1;
第二个未注册的实现是 …/metadata/ReflectiveConfigurationPropertyGenerator.java:73-79)。读取侧,
ConfigurationPropertyLoader.loadAll()(…/metadata/ConfigurationPropertyLoader.java:91-106)对每个 loader
吞掉任何 Throwable 并记录日志(:101-103)—— 这就是模块家族的静默降级策略 —— 而 core 只发布一个 loader,
AdditionalMetadataResourceConfigurationPropertyLoader,注册于
microsphere-java-core/src/main/resources/META-INF/services/io.microsphere.metadata.ConfigurationPropertyLoader:1,
读取 META-INF/microsphere/additional-configuration-properties.json
(…/metadata/AdditionalMetadataResourceConfigurationPropertyLoader.java:36,名字定义在
microsphere-java-core/src/main/java/io/microsphere/constants/ResourceConstants.java:60)。
| 模式 | 实现类型 | 证据 |
|---|---|---|
| 标记注解(marker annotation) | @Immutable、@Nonnull、@Nullable、@ThreadSafe、@NotThreadSafe —— 空体,无属性 |
annotation/Immutable.java:33-34、annotation/Nonnull.java:36-37、annotation/Nullable.java:38-39、concurrent/ThreadSafe.java:29-30 |
| 元注解 / 组合注解 | @Nonnull 与 @Nullable 把 @javax.annotation.Nonnull + @TypeQualifierNickname 组合到自己身上 |
annotation/Nonnull.java:34-35、annotation/Nullable.java:36-37 |
| 昵称 / 别名(JSR-305 惯用法) | 一个 provider qualifier,两个公开名字,用 when 参数区分 |
annotation/Nullable.java:24、:36 |
| 以注解立约(contract-by-annotation) | 字段声明自己的配置契约;强制执行并发布它的是处理器,不是字段的主人 | microsphere-java-core/src/main/java/io/microsphere/beans/BeanUtils.java:118-124 → …/processor/ConfigurationPropertyJSONElementVisitor.java:70-107 |
| 注解作为值载体(复合) | 另一个注解暴露 Since since() 和 ConfigurationProperty[] properties() |
microsphere-java-test/src/main/java/io/microsphere/test/annotation/TestAnnotation.java:69、:71 |
| 默认值推断(null-object 的一个变体) | 省略的属性被元素自身的事实替代 | …/processor/ConfigurationPropertyJSONElementVisitor.java:119-136 |
javax.lang.model 上的访问者 |
AnnotatedElementJSONElementVisitor 继承 + visitVariableAsField 覆写 |
microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/AnnotatedElementJSONElementVisitor.java:88、:120;覆写位于 …/ConfigurationPropertyJSONElementVisitor.java:69-70 |
| SPI + 服务定位 | generator/loader/reader 通过 META-INF/services 发现 |
…/ConfigurationPropertyJSONElementVisitor.java:65;…/metadata/ConfigurationPropertyLoader.java:93 |
| 成对相反数 | ThreadSafe ⇄ NotThreadSafe、Nonnull ⇄ Nullable、Since ⇄ Experimental(@see Experimental) |
concurrent/ThreadSafe.java:24、concurrent/NotThreadSafe.java:23、annotation/Since.java:39 |
| 按受众分层 retention | 刻意做出的 SOURCE / CLASS / RUNTIME 划分 |
§2 清单表 |
@Experimental在构造上就是不可见的。SOURCE(annotation/Experimental.java:37)意味着没有类文件、 没有反射、没有字节码工具。它自己的测试就在断言这种不可见性 (src/test/java/io/microsphere/annotation/ExperimentalTest.java:35)。如果意图哪天变成“在运行期告警”, retention 就必须迁移到RUNTIME—— 对缓存了SOURCE行为的工具而言这是一个二进制不兼容的决定。@Experimental被称作“marker annotation”,但它并不是。 javadoc 开头写着 “The marker annotation indicates a feature is experimental”(annotation/Experimental.java:31), 而类型却声明了description()(:50)。marker = 无属性;这是一个描述型注解。同样的措辞漂移还出现在annotation/ConfigurationProperty.java:65,那里的 javadoc 标签被拼成了@reurn。@Nullable的 javadoc 指向了“错的”兄弟。@see javax.annotation.Nonnull(annotation/Nullable.java:31) —— 技术上没错(那确实是它别名的 qualifier),但和annotation/Nonnull.java:29引用同一个类型并列时很令人困惑。 两处都应该把when的差异写清楚。CLASSretention 让concurrent那一对在运行期不可观测,而到目前为止在工具侧也不可观测,因为它不带 JSR-305 别名(§4 第 1 条)且使用次数为零。@Documented(:26/:25)是它目前唯一生效的地方。- 五个
RUNTIME类型里有三个没有@Target(Nonnull、Nullable、Immutable),这是语言所能给出的最宽 适用范围 —— 对作者方便,但事后无法收回:补一个受限的@Target,对所有已经在你打算排除的位置上使用它的人来说 都是一次破坏源码的变更。它同时也悄悄堵死了 type-use 放置,而那正是现代空值工具更偏爱读取的位置。 concurrent那一对省略了@author和@since(concurrent/ThreadSafe.java:17-25、concurrent/NotThreadSafe.java:17-24),而根包下六个类型全都带着@since 1.0.0(annotation/ConfigurationProperty.java:33及其兄弟)。任何基于@sincejavadoc 生成“API 历史”的工具都会跳过 这一对。- 注解从不抛异常 —— 也从不强制执行。 与全库的静默降级策略一致,没有任何承诺会在运行期被检查。core 真需要
检查时是手写出来的:
@Nonnull String name与assertNotNull(...)配对 (microsphere-java-core/src/main/java/io/microsphere/beans/ConfigurationProperty.java:115、:119-120)。 请把这种配对当作新 API 的约定。 io.microsphere.lang.Deprecation不读@Since。 它的getSince()返回的是从自身模型解析出的Version(microsphere-java-core/src/main/java/io/microsphere/lang/Deprecation.java:111),而该类只导入了Immutable/Nonnull/Nullable(:19-21)。这两套生命周期机制是平行的,并未打通。
位置:microsphere-java-annotations/src/test/java/io/microsphere/annotation/ —— 六个 JUnit 5 类,每个属性或非平凡
retention 值得钉住的注解一个类(ThreadSafe/NotThreadSafe 没有测试,这与它们没有可观测的运行期行为相一致)。
| 测试 | 钉住什么 | 证据 |
|---|---|---|
NonnullTest / NullableTest / ImmutableTest |
RUNTIME retention 是真的:类级注解可以取回 |
NonnullTest.java:31 + :36;NullableTest.java:31 + :36;ImmutableTest.java:31 + :36 |
ExperimentalTest |
SOURCE retention 是真的:同一个调用返回 null |
ExperimentalTest.java:30 + :35(assertNull) |
SinceTest |
两个属性能完整反射往返,包括必填的 value() |
SinceTest.java:30 + :35-37 |
ConfigurationPropertyTest |
三个 source() 常量是精确字符串 |
ConfigurationPropertyTest.java:38-40 |
Tip
改动本模块时应补上的缺口。(1)没有测试断言 JSR-305 别名可以解析 —— 请添加
assertNotNull(Nonnull.class.getAnnotation(javax.annotation.Nonnull.class)) 以及 when = MAYBE 的等价断言;
jsr305 已经在本模块的 classpath 上(microsphere-java-annotations/pom.xml:24-28)。(2)没有测试断言
@Nonnull 在参数或方法上同样被保留,而不只是在类型上。(3)没有测试断言 @Target 的内容 ——
core 里已经用这种方式做过
Since.class.getAnnotation(Target.class)
(microsphere-java-core/src/test/java/io/microsphere/util/AnnotationUtilsTest.java:473)。
(4)赋予 @ConfigurationProperty 价值的那些处理器行为,是在 microsphere-annotation-processor 和
microsphere-annotation-test 的 javac 框架里测试的,而不是这里 —— 请保持现状,因为本模块绝不能增加对 core 的
测试依赖。
- 新增第九个注解 —— 保持这些不变式:不引入仓库内依赖、不产生类文件、retention 要有意识地选
(只有在必须有东西实时读取它时才默认
RUNTIME)、永远加@Documented、永远加@sincejavadoc, 并在同一次变更里更新两份指南各自的清单表。 - 启用包级别可空性 —— 唯一缺失的 JSR-305 部件就是
@TypeQualifierDefault(§3.2)。在这里加入package-info.java会改变 99 个已标注文件的含义,所以它必须伴随一次分析器配置变更和一条 release note 落地, 而不是悄悄发生。 - 让
concurrent那一对真正有牙齿 —— 要么删掉concurrent/ThreadSafe.java:9和concurrent/NotThreadSafe.java:9,要么把它们变成@javax.annotation.concurrent.ThreadSafe/…NotThreadSafe元注解,好让字节码分析器认账。 - 扩展元数据流水线 —— 实现
ConfigurationPropertyGenerator(JSON 形状)、ConfigurationPropertyLoader(额外来源)或ConfigurationPropertyReader(额外格式),通过META-INF/services注册;loadAll()已经能容忍失败的 loader (microsphere-java-core/src/main/java/io/microsphere/metadata/ConfigurationPropertyLoader.java:101-103)。
- Microsphere Java Annotations —— 用户指南 —— 逐注解的契约与陷阱
- 配置属性元数据 —— 开发者指南 —— 生成 JSON 下游的那些读取方
- 语言抽象 —— 开发者指南 ——
Deprecation、io.microsphere.util.AnnotationUtils及其isMetaAnnotation一族(.../util/AnnotationUtils.java:535) - microsphere-annotation-processor —— 编译期消费方
- 英文版:English Developer Guide
- 模块索引