模块:io.github.microsphere-projects:microsphere-annotation-processor · 0.3.20-SNAPSHOT(release 分支)
包:io.microsphere.annotation.processor(4 个主类型,4 个测试类 + 1 个 SPI 夹具)· Java 8 基线,CI 覆盖 JDK 8 / 11 / 17 / 21 / 25
下文使用的路径前缀:microsphere-annotation-processor/src/main/java/io/microsphere/(主源码,引用时写作
annotation/processor/…)、microsphere-annotation-processor/src/test/java/io/microsphere/(测试),其余一律使用完整的
microsphere-<module>/src/... 路径。
元数据特性被拆成两个模块、两个数据流方向。本模块在 javac 运行期间写出
META-INF/microsphere/configuration-properties.json;microsphere-java-core 里的 io.microsphere.metadata 在运行时
读取它,而该包的某个 SPI(ConfigurationPropertyLoader)会在编译期把属性回灌进本模块的产出。IDE 是同一份文件的
第三类消费者。
graph TD
AN["@ConfigurationProperty<br/>microsphere-java-annotations"] --> AP["microsphere-annotation-processor<br/>ConfigurationPropertyAnnotationProcessor"]
AP --> LM["microsphere-lang-model<br/>JSONElementVisitor family<br/>AnnotationUtils · MessagerUtils"]
AP --> CORE["microsphere-java-core<br/>beans.ConfigurationProperty<br/>metadata.ConfigurationPropertyGenerator/Loader<br/>json.JSONArray · constants"]
AP ==> |"shade:原样嵌入,无重定位<br/>pom.xml:105, :115"| SHADED["io.microsphere.** 类被装进<br/>已发布的处理器 jar"]
AP --> RES["META-INF/microsphere/configuration-properties.json<br/>(CLASS_OUTPUT → 进入你的 jar)"]
AP -.test.-> AT["microsphere-annotation-test<br/>AbstractAnnotationProcessingTest<br/>CompilerInvocationInterceptor"]
AT --> JT["microsphere-jdk-tools<br/>jdk.tools.compiler.Compiler"]
RES --> RT["运行时:io.microsphere.metadata<br/>MetadataResourceConfigurationPropertyLoader<br/>DefaultConfigurationPropertyReader"]
RT --> IDE["IDE / 工具链:补全 + 文档浮窗"]
LOADERSPI["META-INF/services/io.microsphere.metadata.ConfigurationPropertyLoader"] --> AP
由源码证明的边:
pom.xml:24-28声明microsphere-java-core(compile),:31-35声明microsphere-lang-model;:51-56声明microsphere-annotation-test且为test作用域(:53、:55)。- processor → lang-model —
ConfigurationPropertyJSONElementVisitor extends AnnotatedElementJSONElementVisitor(annotation/processor/ConfigurationPropertyJSONElementVisitor.java:57,导入在:23),而后者继承JSONElementVisitor(microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/AnnotatedElementJSONElementVisitor.java:88); 静态导入见ConfigurationPropertyJSONElementVisitor.java:38-43与ConfigurationPropertyAnnotationProcessor.java:43。 - processor → core — 模型 bean(
ConfigurationPropertyJSONElementVisitor.java:22)、JSONArray写出 (ConfigurationPropertyAnnotationProcessor.java:22,使用于:151-152)、ResourceConstants(:21、:39)、SymbolConstants(:40-42)、ConfigurationPropertyGeneratorSPI(:23)以及ConfigurationPropertyLoader.loadAll(:44,调用点:135)。 - 运行时消费者 —
MetadataResourceConfigurationPropertyLoader指向完全相同的常量 (microsphere-java-core/src/main/java/io/microsphere/constants/ResourceConstants.java:54),而DefaultConfigurationPropertyReader解析的正是这里产出的数组形状。这些 reader 的分析见 配置属性元数据 — 开发者指南。
Note
annotation-processor → core 这条边既是 Maven 边也是运行时边:处理器在它自己运行期间就需要 core 的模型、JSON 与
SPI 类,也就是它们必须位于处理器 classpath 上。这正是 pom.xml:103-121 的 maven-shade-plugin 配置解决的问题——
也正是给同时依赖 core 的使用者制造重复类问题的根源(§9)。
| 类型 | 可见性 | 职责 | 关键成员 |
|---|---|---|---|
ConfigurationPropertyAnnotationProcessor(annotation/processor/ConfigurationPropertyAnnotationProcessor.java:78) |
public |
Java EE 的 Processor:逐轮累加、末轮写出、诊断输出 |
init :88-95、process :97-105、resolveMetadata :107-110 / :112-132、appendGeneratedConfigurationPropertyJSON :134-140 / :142-147、writeMetadata :149-156、toJSON :158-160、getSupportedSourceVersion :162-165 |
ConfigurationPropertyJSONElementVisitor(ConfigurationPropertyJSONElementVisitor.java:57) |
包私有 | 把一个被标注的 VariableElement 变成一个 JSON 对象 |
常量 :59、构造器 :63-66、visitVariableAsField :68-107、supportsType :109-112、getGenerator :114-117、各解析方法 :119-141、setSources :143-152、setDeclaredClass/Field :154-163 |
FilerProcessor(annotation/processor/FilerProcessor.java:76) |
public |
带异常处理器地访问 Filer;通过反射提供 JavaFileManager 逃生口 |
构造器 :80-82、processInFiler :84-90 / :92-96、getJavaFileManager :103-106 |
ResourceProcessor(annotation/processor/ResourceProcessor.java:84) |
public |
限定 Location 的资源读写,带 FileObject 缓存与类型化 I/O 回调 |
标志 :86-88、构造器 :100-110、processInResource :112-118 / :120-149、getResource :151-154、InputStream/Reader/CharSequence :156-213、OutputStream/Writer :215-252、exists :254-256 |
测试:ConfigurationPropertyAnnotationProcessorTest(1 个测试)、ConfigurationPropertyJSONElementVisitorTest
(3 个测试 + 7 个被编译的类)、FilerProcessorTest(3 个测试)、ResourceProcessorTest(15 个测试),另有
TestConfigurationPropertyLoader 夹具,注册在
src/test/resources/META-INF/services/io.microsphere.metadata.ConfigurationPropertyLoader。
名字里带 “processor” 的两个类其实不是 Processor 实现——它们是 helper。只有
ConfigurationPropertyAnnotationProcessor 实现了 javax.annotation.processing.Processor(经由
AbstractProcessor 间接实现,ConfigurationPropertyAnnotationProcessor.java:78)。
process() 刻意写得极简(ConfigurationPropertyAnnotationProcessor.java:97-105):
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
writeMetadata();
} else {
resolveMetadata(roundEnv);
}
return false;
}这七行导出五个事实:
annotations参数被忽略。 哪些注解到来了,是由@SupportedAnnotationTypes(:77)决定的,而不是这一轮的集合—— 因此无论目标注解是否存在,处理器每轮都会运行一次。- 用的是根元素,不是被标注元素。
resolveMetadata(RoundEnvironment)调用roundEnv.getRootElements()(:108),并把每个元素交给访问器(:117-121)。getElementsAnnotatedWith从未使用;过滤是访问器的职责。后果:只有本轮编译的元素才可能被写出,因此依赖 jar 里被标注的字段在构造上就是不可见的。 - 累加器状态跨轮次存活。
jsonBuilder在init()中只创建一次(:92),而每个非末轮都会给自己的产出套上一对新的[…](:113、:126-131)。于是有两个非空轮次的编译就会得到[…][…],那不是 JSON 数组——见 §9。 - 写出恰好发生一次,在末轮。
writeMetadata()(:149-156)由roundEnv.processingOver()(:99)把门;普通轮次 什么都不写,所以在末轮之前失败的编译不会留下任何资源。 return false(:104)意味着注解未被认领。 这是多个处理器共享同一注解时的惯常选择,也是为什么在开启-proc:only/-Xlint:processing的构建里,即使处理成功,javac 仍可能为该注解打印 “No processor claimed any of these annotations” 警告。
init()(:88-95)立即构造三个协作者:Messager(:91)、累加器(:92)、绑定到
StandardLocation.CLASS_OUTPUT 的 ResourceProcessor(:93,导入在 :46)以及访问器(:94)。
ResourceProcessor 与访问器的构造器都可能抛异常(§9),这就是配置错误的处理器路径会表现为
“Annotation processor … failed to initialize” 而不是“资源缺失”的原因。
两个包私有方法 resolveMetadata(Set)(:112)与 toJSON()(:158)是为测试而存在的;它们是进入累加逻辑的唯一缝隙。
一次完整编译:普通轮次,随后是末轮:
sequenceDiagram
participant JC as javac
participant PR as ConfigurationPropertyAnnotationProcessor
participant RE as RoundEnvironment
participant VI as ConfigurationPropertyJSONElementVisitor
participant GE as ConfigurationPropertyGenerator SPI
participant JB as jsonBuilder StringBuilder
participant RS as ResourceProcessor
participant FP as FilerProcessor
participant FI as Filer
participant FO as FileObject resource
participant MS as Messager
JC->>PR: init(processingEnv) :88-95
PR->>PR: messager = getMessager() :91
PR->>RS: new ResourceProcessor(processingEnv, CLASS_OUTPUT) :93
PR->>VI: new ConfigurationPropertyJSONElementVisitor(processingEnv) :94
VI->>GE: loadFirstService(ConfigurationPropertyGenerator.class) :65
Note over JC,PR: round N, processingOver() == false
JC->>PR: process(annotations, roundEnv) :98
PR->>RE: getRootElements() :108
RE-->>PR: Set of root TypeElements
PR->>JB: append '[' :113
loop each root element :117-121
PR->>VI: element.accept(visitor, jsonBuilder) :120
VI->>VI: visitType → visitMembers → visitVariableAsField :70
VI->>GE: generator.generate(configurationProperty) :101
GE-->>VI: one JSON object text
VI->>JB: append(json) + ',' :102-103
end
PR->>PR: ConfigurationPropertyLoader.loadAll() :135
PR->>GE: generator.generate(loadedProperty) :144
GE-->>PR: JSON text
PR->>JB: append + ',' , then ']' replaces trailing ',' :126-131
PR-->>JC: return false (annotation not claimed) :104
Note over JC,PR: final round, processingOver() == true
JC->>PR: process(annotations, roundEnv) :98
PR->>PR: writeMetadata() :149-156
PR->>JB: toString() :151
PR->>PR: new JSONArray(text).toString(2) :151-152
PR->>RS: processInResourceWriter(CONFIGURATION_PROPERTY_METADATA_RESOURCE, …) :150
RS->>FP: processInFiler(callback, handler) :123
FP->>FI: createResource(CLASS_OUTPUT, moduleAndPackage, resourceName) :127
FI-->>FP: FileObject
RS->>RS: cache key W + @ + resourceName :124-129
RS->>FO: openWriter() :244
FO-->>RS: Writer
RS->>FO: write(formatedJSON) + flush() :245-246
PR->>MS: printNote("The generated metadata JSON of @{} : {}", …) :154
MS-->>JC: compiler NOTE (build log)
| 契约 | 代码实际返回什么 | 引用 |
|---|---|---|
getSupportedAnnotationTypes() |
继承自 AbstractProcessor,读取类级别的 @SupportedAnnotationTypes(CONFIGURATION_PROPERTY_ANNOTATION_CLASS_NAME)——恰好一个 FQN:io.microsphere.annotation.ConfigurationProperty |
ConfigurationPropertyAnnotationProcessor.java:77,取值见 ConfigurationPropertyJSONElementVisitor.java:59 |
getSupportedSourceVersion() |
已覆写,返回 SourceVersion.latestSupported()——即当前 JDK 提供的最高级别 |
ConfigurationPropertyAnnotationProcessor.java:162-165,静态导入 :45 |
getSupportedOptions() |
未覆写;AbstractProcessor 的默认实现读取并不存在的 @SupportedOptions 注解并返回空集合。代码中没有任何地方消费 -A… 选项 |
ConfigurationPropertyAnnotationProcessor.java:78-166(不存在该覆写) |
注解本身是 @Target(FIELD) / @Retention(RUNTIME)
(microsphere-java-annotations/src/main/java/io/microsphere/annotation/ConfigurationProperty.java:36-38),含六个属性——
name(:60)、type(:67)、defaultValue(:74)、required(:81)、description(:88)、source(:95)——
以及三个来源常量(:43、:48、:53)。
两点结构性说明:
- 支持的注解类型是以字符串常量而非类字面量声明的,因此即使
microsphere-java-annotations不存在,处理器也能被编译和 加载。但访问器会针对被编译工程的 classpath 立即解析该类型 (microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/AnnotatedElementJSONElementVisitor.java:108-110) 并在解析失败时抛出——因此注解必须是被构建工程的 compile 作用域依赖。 latestSupported()与ElementKindVisitor6基类 (microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/JSONElementVisitor.java:57)组合在一起,意味着处理器 接受任何当前源码级别,而它的分发表只认识 Java 6 时代的元素种类。晚于 Java 6 的种类(record 组件、模块声明)会落到FALSE默认值(:59-61、:130-132)并被丢弃,不产生任何诊断。
flowchart TD
A["element.accept(jsonElementVisitor, jsonBuilder)<br/>ConfigurationPropertyAnnotationProcessor.java:120"] --> B{"visitType: supportsType(e)?<br/>ConfigurationPropertyJSONElementVisitor.java:109-112 → always true"}
B -- "no" --> Z["return FALSE, nothing appended"]
B -- "yes" --> C["super.visitType → ElementKindVisitor6 dispatch<br/>JSONElementVisitor.java:93"]
C --> D["visitMembers(e.getEnclosedElements())<br/>JSONElementVisitor.java:96, :111-119"]
D --> E{"visitVariable: supportsVariable(e)?<br/>JSONElementVisitor.java:71-77"}
E -- "no" --> Z
E -- "yes" --> F{"kind == FIELD?<br/>ElementKindVisitor6 branch"}
F -- "no (param/local/enum constant)" --> Z
F -- "yes" --> G["visitVariableAsField<br/>ConfigurationPropertyJSONElementVisitor.java:70"]
G --> H{"AnnotationMirror present?<br/>:71-72"}
H -- "no" --> Z2["return false :106"]
H -- "yes" --> I["getElementValues(annotation) (WITH_DEFAULT)<br/>:74 · AnnotationUtils.java:1464-1466, :1509-1521"]
I --> J{"attribute name"}
J -- "name :79-81" --> K["resolveName: explicit value,<br/>else field.getConstantValue() :119-122"]
J -- "type :82-84" --> L["resolveType: explicit,<br/>else field.asType() :124-128"]
J -- "defaultValue :85-87" --> M["resolveStringValue: null when default :138-141"]
J -- "required :88-90" --> N["raw boolean cast"]
J -- "description :91-93" --> O["resolveDescription: Javadoc fallback :130-136"]
J -- "anything else :94-96" --> P["setSources: only 'source' is honoured<br/>:143-152"]
K --> Q["new beans.ConfigurationProperty(name) :81"]
L --> R["setType(...) :84"]
M --> R2["setDefaultValue :87"]
N --> R3["setRequired :90"]
O --> R4["setDescription :93"]
P --> R5["metadata.sources.addAll :149"]
Q --> S["setDeclaredClass :98 / :154-158<br/>setDeclaredField :99 / :160-163"]
R --> S
R2 --> S
R3 --> S
R4 --> S
R5 --> S
S --> T["generator.generate(property) :101<br/>DefaultConfigurationPropertyGenerator.java:81-121"]
T --> U["append(json) + COMMA_CHAR :102-103 · return true :104"]
U --> V{"round had at least one root element?<br/>ConfigurationPropertyAnnotationProcessor.java:116"}
V -- "yes" --> W["appendGeneratedConfigurationPropertyJSON<br/>loadAll() :135 → generate :144"]
V -- "no" --> X["loader contributions SKIPPED"]
W --> Y["trailing comma → ']' , else append ']' :126-131"]
X --> Y
值得钉住的机制:
- 每个被标注字段一个 JSON 对象,逐字追加。 访问器直接写入共享的
StringBuilder,并给每条记录补一个逗号 (ConfigurationPropertyJSONElementVisitor.java:101-103);处理器在收尾时把那个尾逗号变成](ConfigurationPropertyAnnotationProcessor.java:126-131)。这里没有逐属性的转义工作——引号由JSONUtils.append负责 (microsphere-java-core/src/main/java/io/microsphere/metadata/DefaultConfigurationPropertyGenerator.java:29-30、:88)。 - 属性顺序就是注解的声明顺序。
getElementValues(annotation)以WITH_DEFAULT委托 (microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/AnnotationUtils.java:1464-1466),并按注解类型的 声明方法顺序构建LinkedHashMap,缺失的值用默认值填充(:1509-1521)。因此name(ConfigurationProperty.java:60)总是第一个条目,这正是ConfigurationPropertyJSONElementVisitor.java:81处延迟创建configurationProperty对其余五个分支安全的原因。代码里没有任何东西强制这个顺序;若注解类型的属性顺序被调整,:82-96的其他分支就会 NPE。 - “未指定”等于“推导出来”,而不是“省略”。
matchesDefaultAttributeValue(AnnotationUtils.java:1145-1150)控制着三处推导:name → 字段的常量值(:119-122),type →field.asType()再经TypeUtils.getTypeName解析(:124-128,导入在:43),description →elements.getDocComment(:130-136)。defaultValue用同一个判断反向得到null,generator 随后省略该键(:138-141+DefaultConfigurationPropertyGenerator.java:100-104)。 declaredClass/declaredField总是被填充,分别取自外层元素的类型名和字段的简单名(:98-99、:154-163)—— IDE 跳转到的就是这一对值。- generator 是一次运行期 SPI 查找,每个访问器实例解析一次。
loadFirstService(ConfigurationPropertyGenerator.class)(ConfigurationPropertyJSONElementVisitor.java:65,导入在:44), 同一个实例通过getGenerator()被复用于 loader 贡献的属性 (:114-117→ConfigurationPropertyAnnotationProcessor.java:143-144)。由于DefaultConfigurationPropertyGenerator.getPriority()返回MIN_PRIORITY(DefaultConfigurationPropertyGenerator.java:161-164),而Prioritized.MIN_PRIORITY是Integer.MAX_VALUE(microsphere-java-core/src/main/java/io/microsphere/lang/Prioritized.java:86),MAX_PRIORITY是Integer.MIN_VALUE(:81),NORMAL_PRIORITY是0(:91),任何额外注册的 generator 都会排在默认实现之前并静默改变输出格式。 io.microsphere.json被用了两次,且并不对称。 生成阶段是 generator 里的字符串拼接,本模块唯一真正的 JSON API 调用发生在ConfigurationPropertyAnnotationProcessor.java:151-152(new JSONArray(...).toString(2))。JSON 模型属于 core,不是 Jackson 也不是 Gson。
FilerProcessor 独占对 Filer 的唯一交互(annotation/processor/FilerProcessor.java:92-96):
public <T> T processInFiler(ThrowableFunction<Filer, T> filerCallback, BiFunction<Filer, Throwable, T> exceptionHandler) {
Filer filer = processingEnv.getFiler();
return filerCallback.execute(filer, exceptionHandler);
}单参数重载提供的是降级处理器:一条 mandatory warning 加上 null(:84-90,经由
MessagerUtils.printMandatoryWarning,见
microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/MessagerUtils.java:174-176)。
getJavaFileManager() 通过反射读取 Filer 实现类的私有字段 fileManager(:103-106,
FieldUtils.getFieldValue(true, filer, "fileManager"))——这是对 javac 内部实现的显式依赖,好在它被包装成返回 null
而不是抛错。
ResourceProcessor 在其之上叠加了 Location、缓存与类型化 I/O(annotation/processor/ResourceProcessor.java:84):
- 每个实例绑定一个
Location与一个moduleAndPackage(:90-94);处理器使用的实例是CLASS_OUTPUT加""(ConfigurationPropertyAnnotationProcessor.java:93、ResourceProcessor.java:100-102),因此资源名就是完整的 classpath 相对路径。 - 一个以
("W"|"R") + "@" + resourceName为键的Map<String, FileObject>缓存(:98、:123-130)——同一轮编译里第二次 写同名资源会复用句柄,而不会再碰Filer.createResource,这使processInResourceWriter(:235-252)在轮次内是幂等的, 在构建之间不是。 - 写使用
filer.createResource(location, moduleAndPackage, name),读使用filer.getResource(...)(:127-128)。读路径还会 额外检查exists(resource),不存在时打印一条NOTE并把Optional.empty()交给回调(:132-143)。exists的判据是getLastModified() > 0(:254-256)——因此内存中的FileObject可能被判定为不存在。 - 每个回调体都运行在 try-with-resources 中并显式
flush()(:224-232、:243-251)。 - 默认处理器会抛。 与
FilerProcessor不同,ResourceProcessor的便捷重载会告警并且throw wrap(e, RuntimeException.class)(:112-118、:156-162、:176-182、:195-201、:215-220、:235-240),而 外层processInResource处理器把任何Filer层失败也送进同一个处理器(:145-148)。
于是元数据写出路径是:writeMetadata()(ConfigurationPropertyAnnotationProcessor.java:149-156)→
processInResourceWriter → processInResource(FOR_WRITING) → FilerProcessor.processInFiler →
Filer.createResource(CLASS_OUTPUT, "", "META-INF/microsphere/configuration-properties.json") →
FileObject.openWriter() → writer.write(formatedJSON) → flush → 关闭 → printNote(messager, …)
(MessagerUtils.java:103-105)。这条路径上有两个错误通道,而它们彼此矛盾:Filer 层的那个降级为 null + 警告,
回调层的那个重抛进 process() 并中断编译。
| 模式 | 证明类 | 证据 |
|---|---|---|
访问者 Visitor(对 javax.lang.model 元素的双重分派) |
ConfigurationPropertyJSONElementVisitor |
element.accept(jsonElementVisitor, jsonBuilder)(ConfigurationPropertyAnnotationProcessor.java:120)、visitVariableAsField 覆写(ConfigurationPropertyJSONElementVisitor.java:68-107)、成员遍历 JSONElementVisitor.java:111-119 |
模板方法 Template Method(final 分派 + supports*/doVisit* 钩子) |
JSONElementVisitor(lang-model),具体子类是 ConfigurationPropertyJSONElementVisitor |
visitType 是 final,依次调用 supportsType、super.visitType、visitMembers(JSONElementVisitor.java:87-101);钩子 :130-197;子类恰好覆写一个钩子(ConfigurationPropertyJSONElementVisitor.java:109-112) |
外观 Facade(对 javax.annotation.processing.Filer) |
FilerProcessor 与 ResourceProcessor |
FilerProcessor.java:76-107 隐藏 getFiler() 与异常处理;ResourceProcessor.java:84-257 隐藏 Location、存在性、缓存与流生命周期 |
服务提供者 SPI(Java EE 的 Processor 契约本身,加上库自己的两个 SPI) |
ConfigurationPropertyAnnotationProcessor |
按名字注册在 src/main/resources/META-INF/services/javax.annotation.processing.Processor:1;ConfigurationPropertyJSONElementVisitor.java:65(ConfigurationPropertyGenerator)与 ConfigurationPropertyAnnotationProcessor.java:135(ConfigurationPropertyLoader)是发生在编译期的第二、第三个 SPI 查找 |
| 策略 Strategy(单个属性渲染可替换) | ConfigurationPropertyJSONElementVisitor.generator |
ConfigurationPropertyGenerator 字段及其两个已发布实现(DefaultConfigurationPropertyGenerator.java:78-165、ReflectiveConfigurationPropertyGenerator),按优先级解析 |
| 回调 / 带失败处理器的命令 | FilerProcessor.processInFiler、ResourceProcessor.processInResource |
ThrowableFunction/ThrowableConsumer 与 BiFunction/BiConsumer 成对出现(FilerProcessor.java:22-27、:84-96;ResourceProcessor.java:22-23、:120-149) |
| 空对象 / 静默降级 | FilerProcessor 默认处理器;ResourceProcessor 读分支 |
null + mandatory warning(FilerProcessor.java:84-90);资源缺失时 Optional.empty() + NOTE(ResourceProcessor.java:136-142) |
| 近似 Builder 的累加 | ConfigurationPropertyAnnotationProcessor.jsonBuilder |
单个 StringBuilder 字段(:82)跨轮次被改写(:113-131)——没有中间对象模型的累加 |
四个测试类都继承 AbstractAnnotationProcessingTest
(io/microsphere/annotation/processor/ConfigurationPropertyAnnotationProcessorTest.java:34),它通过
@ExtendWith(CompilerInvocationInterceptor.class) 挂载 JUnit 5 扩展
(microsphere-annotation-test/src/main/java/io/microsphere/test/annotation/processing/AbstractAnnotationProcessingTest.java:53)。
拦截器在每个测试方法执行前,用进程内的 io.microsphere.jdk.tools.compiler.Compiler
(.../CompilerInvocationInterceptor.java:54-55、:63)编译测试类加上 addCompiledClasses
(:133-134)提供的类,注入一个驱动处理器(:57-58),随后追加在测试 classpath 上发现的全部 Processor
(:60-61)——本模块的处理器正是这样被使用的:它的发现方式与真实消费者的构建完全一致。tools.jar 的差异交给
microsphere-jdk-tools,因此 JDK 8 与 JDK 25 走同一条代码路径。
| 套件 | 它真正钉住了什么 | 行数 |
|---|---|---|
ConfigurationPropertyAnnotationProcessorTest |
resolveMetadata(Set) 在空集合上不抛异常并产出非空字符串;init() 与 processingEnv 的装配 |
:36-43 |
ConfigurationPropertyJSONElementVisitorTest |
注解 FQN 常量;supportsType 对 NULL_TYPE_ELEMENT 也返回 true;setSources 对未知属性名是空操作且容忍 null 参数;为喂给该轮次而编译的七个类 |
:46-74 |
FilerProcessorTest |
经包装器调用 createSourceFile;新建的源文件上 exists(...) 为 false;抛异常的回调返回 null;getJavaFileManager() 通过反射解析成功 |
:50-68 |
ResourceProcessorTest |
针对 CLASS_OUTPUT 与 SOURCE_PATH 的读写对称性、缺失资源的 Optional.empty() 分支、每个默认处理器的 assertThrows(RuntimeException.class, …) 以及自定义处理器返回 null 的变体、资源清理 |
:56-194(清理 :71-74,抛异常用例 :82-90、:106-114、:124-132、:141-149、:159-169、:179-189,exists(null) :191-194) |
TestConfigurationPropertyLoader |
夹具:为每个 JDK 简单类型生成一个属性,带随机 sources/targets,用以证明 loader → generator → 文件这条路径 |
io/microsphere/annotation/processor/TestConfigurationPropertyLoader.java:41-71 |
改动本模块之前需要补齐的缺口:
- 没有对生成内容的断言。 唯一的“空场景”测试用的是
assertNotNull(ConfigurationPropertyAnnotationProcessorTest.java:42),于是[]、[,与畸形 JSON 在那里无法区分。toJSON()(ConfigurationPropertyAnnotationProcessor.java:158-160)之所以是包私有,正是为了方便补上内容断言。 visitVariableAsField没有直接测试——六个属性分支 (ConfigurationPropertyJSONElementVisitor.java:79-96)只被间接覆盖,而name先于type的顺序假设(§5)完全未被测试。writeMetadata()未被测试(:149-156):多轮次拼接(§9)与FileAlreadyExistsException路径都没有覆盖。- 没有测试在 generator SPI 缺席的情况下运行处理器
(
ConfigurationPropertyJSONElementVisitor.java:65),而这是本模块最灾难性的失败模式。
-
getSupportedSourceVersion()返回latestSupported()(ConfigurationPropertyAnnotationProcessor.java:164), 而已发布字节码级别来自外部父 POMmicrosphere-build:0.3.16(整个 reactor 中不存在任何maven.compiler.*/<release>属性)。因此处理器向 javac 承诺了最新源码级别,自身却是按 Java 8 API 编译、并通过ElementKindVisitor6分派的 (microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/JSONElementVisitor.java:57)。 稳妥的读法:对已标注的static final字段而言,更新的源码级别是可用的,但晚于 Java 6 的元素种类会被静默跳过而不是 被拒绝。把级别钉成SourceVersion.RELEASE_8会破坏 JDK 25 上的构建;保持latestSupported()是务实选择——但应当配一个visitUnknown诊断。 -
多轮次拼接是一个潜伏的构建破坏者。
jsonBuilder从不重置 (ConfigurationPropertyAnnotationProcessor.java:92),而每个非末轮都会闭合自己的数组(:113、:126-131),于是第二个 非空轮次产生"[…][…]"。末轮接着执行new JSONArray(text)(:151),异常会被ResourceProcessor的默认写处理器包装后 重抛(ResourceProcessor.java:235-240)——异常逃出process(),即与库的静默降级策略相反,而任何一个共存的生产源码的 处理器都可能触发它。 -
loader 贡献被“是否存在注解字段”卡住。
appendGeneratedConfigurationPropertyJSON只在if (!elements.isEmpty())内部被调用(ConfigurationPropertyAnnotationProcessor.java:116,调用点:123),所以没有 被标注字段的模块产出[]并丢弃所有ConfigurationPropertyLoader结果。结合第 2 点,这个调用点还是按轮次的,因此只要 有多个含根元素的轮次,loader 属性就会被重复追加。 -
静默失败模式(每一种都会产出构建成功但元数据错误或缺失的结果):
FilerProcessor.processInFiler把任何Filer失败吞成null+ mandatory warning(FilerProcessor.java:84-90)。- 读路径上的资源缺失变成
Optional.empty()+NOTE(ResourceProcessor.java:136-142)。 exists()依赖getLastModified() > 0(:254-256),因此内存中或刚创建的对象会被判为不存在 (FilerProcessorTest.java:52-54断言的正是这一点)。- 未知的注解属性落入
setSources,而它除"source"之外一概忽略 (ConfigurationPropertyJSONElementVisitor.java:94-96、:143-152)——给@ConfigurationProperty新增属性会静默地产出 零元数据,而不报编译错误。 - 镜像不存在时
visitVariableAsField返回false(:106)——这是正常且正确的路径。 ConfigurationPropertyLoader.loadAll()会记录日志并丢弃抛异常的 loader (microsphere-java-core/src/main/java/io/microsphere/metadata/ConfigurationPropertyLoader.java:101-103),于是编译期贡献 可能只剩一行error日志就消失了。- 根元素迭代没有排序(
ConfigurationPropertyAnnotationProcessor.java:117-121),因此 JSON 中的条目顺序取决于 javac 集合的 顺序;本代码并不保证跨 JDK 版本的逐字节可重现性。
-
本模块的每一个抛出点,对照已记录的静默降级策略:
位置 抛出 触发时机 策略判定 ConfigurationPropertyJSONElementVisitor.java:65IllegalArgumentException(经ServiceLoaderUtils.java:599-604)未注册任何 ConfigurationPropertyGenerator违反——直接杀死构建 AnnotatedElementJSONElementVisitor.java:103-104经 Assert.assertNotNull抛IllegalArgumentException/NullPointerExceptionprocessingEnv或annotationClassName为null可接受(编程错误) AnnotatedElementJSONElementVisitor.java:110Assert.assertNotNull被构建工程的 classpath 上没有该注解类型 违反——表现为“processor failed to initialize” ConfigurationPropertyJSONElementVisitor.java:84-96NullPointerExceptionname不是元素值的第一个条目(§5)违反——脆弱 ConfigurationPropertyJSONElementVisitor.java:81对 null名字的断言(beans/ConfigurationProperty.java:120)非 String常量字段却未写name违反 ConfigurationPropertyJSONElementVisitor.java:89NullPointerException/ClassCastException(boolean) annotationValue.getValue()拆箱违反 ConfigurationPropertyJSONElementVisitor.java:121ClassCastException常量不是 String时的(String) value违反 ConfigurationPropertyAnnotationProcessor.java:151JSON 解析失败 累加结果畸形(§9.2) 违反 ResourceProcessor.java:116、:160、:180、:199、:218、:238throw wrap(e, RuntimeException.class)每个读/写便捷方法的默认处理器 设计上违反——传入你自己的处理器即可规避( :120-149、:164-174、:184-193、:204-213、:222-233、:242-252)FilerProcessor.java:84-90不抛 默认处理器降级为 null符合 十行里有六行在
ResourceProcessor内,并且就读方法而言是有文档的行为,但写方法(:235-240)位于元数据路径上,于是 本模块绝不该失败的那件事——写一个 JSON 文件——恰恰就是会抛的那件事。 -
shade 的后果。
pom.xml:103-121把maven-shade-plugin:shade绑定到package(:108-111),artifactSet恰好只有io.github.microsphere-projects:microsphere-java-core(:113-117),再无其他:没有<relocations>、没有<minimizeJar>、没有<filters>、没有<shadedArtifactAttached>、也没有覆盖<createDependencyReducedPom>。 行为因此就是插件的默认值:core 的类与其META-INF/services/*文件都以原名被复制进处理器 jar,shaded 归档取代target/microsphere-annotation-processor-<version>.jar成为主构件(shade 前的 jar 被改名为original-microsphere-annotation-processor-<version>.jar),并且会写出一份 dependency-reduced POM,其中microsphere-java-core依赖不再出现。因此同时依赖两个构件的使用者,会在 classpath 上得到io.microsphere.**的两份 逐字副本;赢家是 classpath 顺序,而内嵌那份固定在产出本处理器发布版时所用的 core 构建上(pom.xml:24-28,根${revision}在pom.xml:54)。评审者应把 shading 视为针对处理器 classpath 的实现便利(它让单个annotationProcessorPaths条目就足够用),而不是一层公开 API——core 的包名什么都没被重定位,所以这份重复无法靠 exclude core 来规避。 -
需要关注的跨模块耦合。
CONFIGURATION_PROPERTY_ANNOTATION_CLASS_NAME是一个重复的字符串字面量 (ConfigurationPropertyJSONElementVisitor.java:59),而不是ClassUtils.getClassName(ConfigurationProperty.class)—— 注解若挪包,处理器会以Assert失败而非编译错误的形式崩掉。重命名configuration-properties.json(microsphere-java-core/src/main/java/io/microsphere/constants/ResourceConstants.java:43、:54)会同时改变这里的写出 目标、每一个运行时 loader,以及所有手写的伴生文件。
- 面向使用者的参考:注解处理器 — 用户指南
- 运行时那一半:配置属性元数据 — 开发者指南
- 运行时那一半的用户视角:配置属性元数据 — 用户指南
- 邻近模块:microsphere-lang-model · microsphere-annotation-test · microsphere-java-annotations
- 英文版本:English Developer Guide