| 构件 | io.github.microsphere-projects:microsphere-jdk-tools(release 分支上为 0.3.20-SNAPSHOT,已发布为 0.3.20) |
| 类型 | 主源码只有一个类型:io.microsphere.jdk.tools.compiler.Compiler(src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java:72) |
| 依赖 | 仅 microsphere-java-core(microsphere-jdk-tools/pom.xml:24-28,版本 ${revision}) |
| CI 覆盖的 JDK | Temurin 8 / 11 / 17 / 21 / 25(.github/workflows/maven-build.yml:25、:33) |
| 使用的编译器 API | javax.tools.ToolProvider.getSystemJavaCompiler() —— 在 Compiler.java:64 导入,在 :107 调用 |
| 测试面 | src/test/java/io/microsphere/jdk/tools/compiler/CompilerTest.java(11 个测试方法) |
本模块提供的是对 JDK 自带编译器的可移植访问能力:同一段代码可以在 JDK 8 到 25 上执行进程内编译。
它是注解处理测试栈所坐落的那一层:把 Class 对象换算成源文件,在你当前的 JVM 内运行 javac,
并把工作交给注册的注解处理器。
Important
本模块不触碰 com.sun.tools.javac 内部 API。 主源码里没有 tools.jar 路径拼接,没有
Class.forName("com.sun.tools.javac.…"),也没有任何反射。可移植性完全交给标准 javax.tools API,由 JDK 自己在
各版本上做适配。下面那张分 JDK 的行为表,全部源自这一个调用点以及保护它的判空检查。
当测试或工具必须在运行中的 JVM 内编译 Java 源文件、而不是外部调用 javac 进程时,
你需要本模块:
- 注解处理器测试。 用真实类跑你的处理器,并断言它的产物。
microsphere-annotation-test就是这么做的:CompilerInvocationInterceptor构造一个Compiler,把测试涉及的类作为源路径喂进去,注册被测处理器以及通过 SPI 找到的每个Processor,然后调用compile(...)(microsphere-annotation-test/src/main/java/io/microsphere/test/annotation/processing/CompilerInvocationInterceptor.java:54-63)。 - 源码到字节的生成流水线 —— 生成文件、编译、加载。
- 必须在 CI 的五种 JDK 上都能跑的**“这个类还能按我的预期被编译吗”**检查。
具体能力如下:
| 能力 | 入口 |
|---|---|
| 定位 JDK 编译器,缺失时立刻失败 | new Compiler(...) → Compiler.java:103-109 |
| 按源文件编译给定的一组类 | compile(Class<?>...) → Compiler.java:156-167 |
| 注册注解处理器 | processors(Processor...) → :136-139 |
| 收集诊断信息 | diagnosticListener(...) → :141-144 |
| 发现任意类的项目根目录 / 源目录 | detectRootDirectory、detectSourcePath → :264-271、:242-262 |
| 控制 javac 选项、locale、编码 | options、locale、charset → :111-114、:146-154 |
那几个路径发现静态方法(detectClassPath / detectRootDirectory / detectSourcePath)才是本模块存在的理由:
javax.tools 编译的是 File,
而调用方手上通常只有 Class。
Compiler 把每个类映射为 <name>.java
(resolveJavaSourceFileRelativePath,Compiler.java:284-286),在配置的源路径里搜索(:217-226),
并丢弃找不到源文件的类(:209-215)。
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-jdk-tools</artifactId>
<version>0.3.20</version>
</dependency>release 上 ${revision} 为 0.3.20-SNAPSHOT(根 pom.xml:54);groupId 固定在根 pom.xml:12。该版本同样由
反应堆 BOM 管理(microsphere-java-dependencies/pom.xml:38)。
| 仓库内使用方 | 作用域 | POM 证据 |
|---|---|---|
microsphere-annotation-test |
compile | microsphere-annotation-test/pom.xml:33 |
microsphere-lang-model |
test | microsphere-lang-model/pom.xml:46,:48 为 <scope>test</scope> |
microsphere-jdk-tools 自身 |
compile | microsphere-jdk-tools/pom.xml:14 |
microsphere-annotation-processor 对本模块没有直接依赖;
它是通过 microsphere-annotation-test(测试作用域,microsphere-annotation-processor/pom.xml:53)
以及 microsphere-lang-model(microsphere-annotation-processor/pom.xml:33)
间接拿到的。
Note
microsphere-lang-model 声明了该依赖,但该模块的源码与测试里都没有引用
io.microsphere.jdk.tools.compiler.Compiler(已遍历其源码核实)。请把这条依赖边理解为“留给驱动编译的测试”,
而不是运行期要求。
传递依赖里你还会得到 microsphere-java-core,本模块大量使用它的工具类 —— CollectionUtils、Lists、Sets、
SetUtils、FileConstants、SymbolConstants、StringBuilderWriter、SimpleFileScanner、LoggerFactory、
FormatUtils、Assert、ClassUtils、StringUtils(Compiler.java:19-63)。
core 不需要运行期日志实现:它的门面选取第一个可用的
LoggerFactory(microsphere-java-core/src/main/java/io/microsphere/logging/LoggerFactory.java:73-96)。
| 项目 | 取值 | 证据 |
|---|---|---|
Compiler 的 Javadoc @since |
1.0.0 |
Compiler.java:70 |
| 本文档对应的发布线 | release 分支,${revision} = 0.3.20-SNAPSHOT |
根 pom.xml:54 |
| 已发布的标签 | 0.3.20 |
仓库标签;groupId 见根 pom.xml:12 |
| 字节码级别 | 本反应堆未声明 —— 继承自外部父 POM microsphere-build:0.3.16(根 pom.xml:8-9) |
整棵模块树里不存在任何 maven.compiler.* / <release> 属性 |
Warning
不要为本模块引用某个 -source/-target 数字。release 分支没有配置:整个反应堆里唯一的
maven-compiler-plugin 配置是 microsphere-annotation-processor 中的 -proc:none,而
microsphere-jdk-tools 根本没有 <build> 段(microsphere-jdk-tools/pom.xml 到 :66 结束)。
实际级别来自 microsphere-build:0.3.16。
Compiler.java 里没有版本分支 —— 没有 if (javaVersion >= 9),没有 JavaVersion/SystemUtils 判断,也没有
Runtime.version() 调用。真正存在的两处间接分支就是全部故事:
getSystemJavaCompiler()(Compiler.java:107)—— 由 JDK 自己的javax.tools.ToolProvider解析,编译器实现 不可达时返回null;Compiler随即抛异常(:108)。Lists.ofList/Sets.ofSet—— 被options(...)(:112)与processors(...)(:137)使用。若List.of/Set.of存在就绑定它们的方法句柄(microsphere-java-core/src/main/java/io/microsphere/collection/Lists.java:101、Sets.java:99),否则回退到unmodifiableList(asList(…))/ 链表散列集,且回退包在catch (Throwable)里 (Lists.java:520-529、Sets.java:559-568)。这只改变 options/processors 的实现类,不改变 API 约定。
| JDK 8 | JDK 11 | JDK 17 | JDK 21 | JDK 25 | |
|---|---|---|---|---|---|
getSystemJavaCompiler() 的来源(Compiler.java:107) |
由 ToolProvider 从 <jdk>/lib/tools.jar 定位到的 com.sun.tools.javac.Main |
jdk.compiler 模块,javax.tools.JavaCompiler 服务 |
同左 | 同左 | 同左 |
tools.jar |
JDK 中有,JRE 中无 | 不存在 | 无 | 无 | 无 |
Compiler 是否会去找 tools.jar? |
不会 —— 在任何 JDK 上都只对返回的 JavaCompiler 判空(:107-108) |
不会 | 不会 | 不会 | 不会 |
本模块需要 --add-exports / --add-opens 吗 |
不需要 | 不需要 | 不需要 | 不需要 | 不需要 |
| 仅 JRE 的运行环境 | null → IllegalArgumentException(:108) |
镜像中缺 jdk.compiler(例如未包含它的 jlink 镜像)时为 null → 同样异常 |
同左 | 同左 | 同左 |
options(...) / processors(...) 的集合实现(:112、:137) |
Arrays.asList 支撑的不可变列表 / 链表散列集 |
List.of/Set.of(不可变) |
不可变 | 不可变 | 不可变 |
compile(...) 产出的字节码级别 |
运行 JDK 的默认级别,因为从不传 --source/--release(DEFAULT_OPTIONS,:76-77) |
同左 | 同左 | 同左 | 同左 |
源文件发现(:242-271) |
通过 CodeSource 位置,并假定 target/classes 布局 |
同左 | 同左 | 同左 | 同左 |
这一带最常流传的参数是 --add-exports java.compiler/com.sun.tools.javac.*=ALL-UNNAMED。针对 release 分支有两点更正:
- 包
com.sun.tools.javac.*不在模块java.compiler里(该模块只承载标准javax.tools/javax.lang.model/javax.annotation.processingAPI)。实现位于jdk.compiler。 microsphere-jdk-tools从不读取这些包,所以在 CI 矩阵的任何 JDK 上都不需要这类参数。只有当你的代码或某个把JavaCompiler向下转型为com.sun.tools.javac类型的测试框架介入时,参数才有意义。
Tip
CI 就是证据:构建命令为
mvn --batch-mode --update-snapshots --file pom.xml -Drevision=0.0.1-SNAPSHOT -Dsurefire.useSystemClassLoader=false test --activate-profiles test,coverage
(.github/workflows/maven-build.yml:38-45)。既没有传 --add-opens / --add-exports,仓库里也没有
.mvn/jvm.config(只有 .mvn/wrapper/maven-wrapper.properties)。因此 CompilerTest 必须、也确实能在五种
原版 JVM 上运行。若你自己的测试需要模块参数,那是你的代码的要求,与 Compiler 无关。
| 条目 | 值 | 依据 |
|---|---|---|
| 文档所在线 | release 分支,${revision} = 0.3.20-SNAPSHOT |
根 pom.xml:54 |
| 最新已发布版本 | 0.3.20,标签日期 2026-10-16 |
标签 0.3.20 的提交信息为 chore: merge main into release [skip ci],提交时间 2026-10-16T03:43:20Z |
| 上一版本 | 0.3.19,标签日期 2026-08-30 |
标签 0.3.19,提交时间 2026-08-30T12:51:37Z |
| 变更日志来源 | 仓库根目录的 release-notes.md |
本项目不发布 CHANGELOG.md;release-notes.md 的 v0.3.16–v0.3.20 段落写着 Release notes generation failed 并保留原始提交列表,因此下文一律以提交主题引用,并逐条与 release 源码核对。 |
0.3.20 在这一区域没有新增 API 面——0.3.19...0.3.20 区间是一次加固性发布。它实际改变了什么、以及这些改动触及本页源码的程度如下:
- 空值契约。 本页只覆盖一个主源码(
jdk/tools/compiler/Compiler.java),它未声明@Nonnull/@Nullable。 - 该区间没有其它改动触及本页覆盖的区域。
Note
如何读这份提交清单:该区间没有点名 microsphere-jdk-tools 的提交,其唯一类也未声明空值契约 — 0.3.19...0.3.20 中没有条目可归到本模块。
以下签名逐字取自 release。类型为 public class Compiler(Compiler.java:72),设置方法均以 this 支持链式调用。
public class Compiler {
// Compiler.java:76-77
public static final String[] DEFAULT_OPTIONS = {"-parameters", "-Xlint:-unchecked", "-nowarn", "-Xlint:deprecation"};
// 构造器 — Compiler.java:95-109
public Compiler() // 源目录与目标目录均由 Compiler.class 探测
public Compiler(File targetDirectory) // 源目录自动探测,目标目录显式指定
public Compiler(File defaultSourceDirectory, File targetDirectory)
// throws IllegalArgumentException("No Java compiler available. Ensure this process is running
// on a JDK (not just a JRE).") — via Assert.assertNotNull, Compiler.java:108
// 链式配置 — 全部返回 this
public Compiler options(String... options) // :111-114 整体替换列表(不追加)
public Compiler sourcePaths(File... sourcePaths) // :116-119 向集合追加
public Compiler sourcePaths(Class<?>... sourceClasses) // :121-126 追加每个类探测到的源路径
public Compiler sourcePath(Class<?> sourceClass) // :128-134 找不到源文件时为空操作
public Compiler processors(Processor... processors) // :136-139 整体替换集合
public Compiler diagnosticListener(DiagnosticListener<? super JavaFileObject> l) // :141-144
public Compiler locale(Locale locale) // :146-149
public Compiler charset(Charset charset) // :151-154
// 终结操作 — Compiler.java:156-167
public boolean compile(Class<?>... sourceClasses) throws IOException;
// 访问器 — Compiler.java:169-207
public JavaCompiler getJavaCompiler(); // 构造之后永不为 null
public StandardJavaFileManager getJavaFileManager() throws IOException; // 调用方负责关闭
public DiagnosticListener<? super JavaFileObject> getDiagnosticListener(); // 未设置则为 null
public Locale getLocale(); // 未设置则为 null
public Charset getCharset(); // 未设置则为 null
public List<String> getOptions(); // 永不为 null;被清空后为空
public Set<Processor> getProcessors(); // 永不为 null
// 静态发现 — Compiler.java:228-286
public static File detectSourcePath(Class<?> sourceClass); // 找不到源文件返回 null(:242-262)
public static File detectRootDirectory(Class<?> sourceClass); // :264-271
public static File detectClassPath(Class<?> sourceClass); // 抛 UnsupportedOperationException(:273-282)
public static String resolveJavaSourceFileRelativePath(Class<?> sourceClass); // :284-286
static File defaultSourceDirectory() // 包级私有,:228-230
static File defaultRootDirectory() // 包级私有,:232-234
static File defaultTargetDirectory() // 包级私有,:236-240,mkdirs() 不检查返回值
}| 方法 | 来自源码的约定 |
|---|---|
compile(Class...) |
每次调用新建一个 StandardJavaFileManager(:159),把 SOURCE_PATH 设为配置的源目录,CLASS_OUTPUT 与 SOURCE_OUTPUT 都设为那唯一的输出目录(:175-177),传入用于捕获输出的 Writer,调用 task.setProcessors(getProcessors()) 并返回 task.call()(:162-163)。true 表示编译成功。 |
compile 与缺失的源文件 |
找不到 .java 的类会被静默丢弃 —— .filter(Objects::nonNull)(:209-215)。若全部被丢弃,javac 收到空的编译单元列表,compile 通常仍返回 true。 |
getOptions() |
配置项的 unmodifiableList 视图;列表为空时返回 emptyList()(:193-199)。 |
getProcessors() |
unmodifiableSet;从未设置时为 emptySet()(:201-207)。compile() 总会调用 task.setProcessors(getProcessors())(:162),而 javax.tools.CompilationTask 仅在集合非空时才执行注解处理 —— 因此字段保持未设置(:85)意味着完全不处理,而不是“让 javac 从 classpath 自行发现处理器”。请显式注册处理器;这正是 CompilerInvocationInterceptor 先走 SPI 加载它们的原因(microsphere-annotation-test/.../CompilerInvocationInterceptor.java:60-62)。 |
getJavaFileManager() |
该访问器不负责关闭;compile 通过 try-with-resources 关闭(:159),直接调用者同样需要自行关闭。 |
| 可空性 | Compiler.java 完全没有 @Nullable / @Nonnull 注解(文件里唯一的 Javadoc 是 :66-71 的类注释);因此上表就是契约,逐行读自代码。 |
| 线程安全 | 只有 javaCompiler 是 final 且安全发布(:83、:107);其余字段都可变且无同步(:85-93)—— 见陷阱 7。 |
-parameters 会把方法参数名保留进 class 文件 —— 注解处理器以及任何基于反射读取元数据的代码都依赖它。
-Xlint:-unchecked 与 -nowarn 抑制警告输出,而 -Xlint:deprecation 要求输出过时提示;这组选项对 javac 并不矛盾,
但确实意味着警告很少进入被捕获的输出(陷阱 4)。
取自 microsphere-jdk-tools/src/test/java/io/microsphere/jdk/tools/compiler/CompilerTest.java —— 完整配置链加一次编译:
Compiler compiler = new Compiler();
boolean ok = compiler.compile(Compiler.class, Test.class); // ok == true
compiler.options() // 清空 DEFAULT_OPTIONS
.processors(new AbstractProcessor() {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
})
.diagnosticListener(diagnostic -> { })
.locale(getDefault())
.charset(UTF_8);
ok = compiler.compile(Compiler.class);出处:CompilerTest.java:143-170(testCompile)。相关断言:
CompilerTest.java:38-44—— 无参构造得到非 null 的JavaCompiler。CompilerTest.java:86-93——detectClassPath(Compiler.class)存在;detectClassPath(String.class)抛UnsupportedOperationException。CompilerTest.java:107-121——sourcePaths(...)返回同一实例(链式),并同时接受File与Class变参。CompilerTest.java:181-187—— 源文件不在项目树中的类,detectSourcePath返回null(assertNull(detectSourcePath(Test.class)))。
Note
testCompile 内的注释写着“with mocked compiler it's safe to call”(CompilerTest.java:146)。该测试并没有任何
mock —— CompilerTest.java:1-28 未导入 Mockito 类型,mockito-core 只是被声明
(microsphere-jdk-tools/pom.xml:52-56)。测试是真的在 Compiler.java 与 CompilerTest.java 上跑 javac。
- JDK 8 与
tools.jar:是“假定”,不是“检测”。Compiler只调用ToolProvider.getSystemJavaCompiler()(:107)并对结果判空(:108)。它从不读取java.home,从不为<jdk>/lib/tools.jar构建类加载器,也从不报告编译器为什么缺失。在 JDK 8 上这意味着:如果你跑在 JRE 上, 或者你的构建把tools.jar排除在ToolProvider看不见的 uber/shade classpath 之外,你只会得到一条固定消息的 异常,关于缺失 jar 的诊断信息一条也没有。 - 失败方式是抛异常,而不是静默降级。
assertNotNull是 core 的断言工具,抛IllegalArgumentException(microsphere-java-core/src/main/java/io/microsphere/util/Assert.java:149-153)。 消息原文(Compiler.java:108):No Java compiler available. Ensure this process is running on a JDK (not just a JRE).本库其余约定是安静降级(detectSourcePath在:250-253返回null,searchJavaSourceFile在:225返回null);该构造器是本模块唯一刻意的 fail-fast。请在构造点捕获,而不是包在compile外面。 - JRE 与 JDK 的区别。 JRE(以及未包含
jdk.compiler的jlink镜像)触发上一条异常。注意不对称之处:异常发生在 创建Compiler时,所以在 JRE 上先调用Compiler.detectSourcePath(...)是安全的 —— 只有构造器会失败。 - 编译器输出只在 trace 级别可见。
compile()把 javac 的输出写进StringBuilderWriter(:158)然后丢弃; 唯一出口是finally块中的logger.trace(...)(:165)。如果你既没传diagnosticListener(...)(字段初值为null,:89),日志级别又高于TRACE,那么编译失败只会返回false,不带任何解释。要么注册监听器,要么为io.microsphere.jdk.tools.compiler.Compiler打开 trace。 sourcePaths(File...)是追加,options(...)与processors(...)是替换。sourcePaths调用CollectionUtils.addAll写入既有LinkedHashSet(:117,coreCollectionUtils.java:454),而options直接重新赋值 (:112),processors同样(:137)。因此不带参数调用options()会丢掉-parameters—— 对依赖参数名的处理器 是一次静默的行为变更。SOURCE_OUTPUT与CLASS_OUTPUT都指向同一个输出目录(:176-177)。生成的源文件与生成的.class落在同一棵 目录树里,所以删除该目录的clean会连同你想检查的生成源码一起删掉。- 一个实例多线程不受支持。 所有配置字段都是可变且非
final的(:79-93)。两个线程在同一实例上调用compile(...),同时第三个线程重新配置它,就可能让LinkedHashSet的修改(sourcePaths,:117)与searchJavaSourceFile内部的迭代(:219)交织 ——ConcurrentModificationException。请为每个测试/每轮编译使用 独立Compiler,或者在外部加锁。 - 默认输出目录取自本模块的位置,而不是你的项目。
defaultTargetDirectory()是<Compiler.class 的根>/target/generated-classes(:236-240),其根来自detectRootDirectory(Compiler.class)=classPath.parent.parent(:264-271)。Maven 构建下它解析为microsphere-jdk-tools/target/generated-classes;而当类来自本地仓库中的 jar 时,两次getParentFile()会落在 仓库目录布局内部。dir.mkdirs()的返回值未被检查(:238)。如果默认值对你有意义,请显式传入输出目录 ——new Compiler(new File("src/main/java"), new File("target/generated-classes"))。 - 找不到 class path 会抛异常。 当类没有
CodeSource时,detectClassPath抛出UnsupportedOperationException,消息模板为"The source {} is based on the file system, the class path can't be detected."——{}是 coreFormatUtils的占位符(microsphere-java-core/src/main/java/io/microsphere/text/FormatUtils.java:17、:38),由该类替换 (:276-281,文本在:280)。实际对应运行期镜像里的类,或不上报 code source 的类加载器。detectSourcePath与sourcePath(Class)是降级版本(:250-253);detectRootDirectory与Compiler()/Compiler(File)构造器不是 —— 经由defaultSourceDirectory()→detectSourcePath是 null 安全的,但defaultRootDirectory()→detectClassPath(:232-234、:265)会先抛出。 - 源文件查找是一次递归、无边界的全盘扫描。
detectSourcePath用SimpleFileScanner.INSTANCE.scan(rootDirectory, true, …)扫描整个模块根目录(:246-247,coreio/scanner/SimpleFileScanner.java:57、:87),匹配条件是getAbsolutePath().endsWith(relativePath)(:247)。 在大目录树上这是一次昂贵的遍历,而target/或生成源目录里的陈旧副本可能抢先命中 —— 结果取自first(sourceFiles)(:249),其顺序取决于File#listFiles()。 - Windows 路径按字面处理。
detectClassPath用new File(location.getPath())转换 code-source URL (:277-278),绕过了%XX解码与 URI 处理。项目路径包含空格或非 ASCII 字符时,请在 Windows 上先验证再依赖源文件发现。 - 默认选项里没有
-encoding。charset字段初值为null(:93),于是getStandardFileManager(...)(:174) 使用 JVM 默认编码。在默认平台编码非 UTF-8 的环境下,含非 ASCII 字面量的源文件可能改变 javac 的诊断,甚至损坏字符串常量。 请调用.charset(UTF_8)(CompilerTest.java:164就是这么做的)。 - 忘记
.processors(...)会静默跳过你真正想测的东西。compile()无条件传入getProcessors()(:162); 字段未设置时它是emptySet()(:201-207),而CompilationTask只在集合非空时才做注解处理。最终现象是一次 “成功的编译”,没有生成产物,处理器侧的断言也从未触发。 - 编码是以对象而非名称传入的。
getJavaFileManager()把getCharset()——public Charset getCharset()(:189-191)—— 交给JavaCompiler.getStandardFileManager(...),而按javax.tools约定该位置应当是编码名称;本模块在设置方法(:151-154)与调用点(:174)之间不存在Charset→ 名称的转换。 这是一个类型层面的不一致,在依赖非默认编码之前值得核实;预期的取值应当是名称(getCharset().name()),而CompilerTest.java:164-167正是覆盖这条路径的代码。
- microsphere-annotation-test —— 驱动本模块的 JUnit 5 扩展
- microsphere-annotation-processor —— 最常被它编译的那批处理器
- microsphere-lang-model —— 同一工具链的
javax.lang.model门面 - 类加载与构件识别 —— 用户指南 —— core 中同一主题的 JDK 内部访问页面
- microsphere-jdk-tools —— 开发者指南 ——
Compiler.java逐行分析与图示 - 英文版:English User Guide