| Artifact | io.github.microsphere-projects:microsphere-jdk-tools (0.3.20-SNAPSHOT on release, published as 0.3.20) |
| Types | exactly one main type: io.microsphere.jdk.tools.compiler.Compiler (src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java:72) |
| Depends on | microsphere-java-core only (microsphere-jdk-tools/pom.xml:24-28, version ${revision}) |
| JDKs tested | Temurin 8 / 11 / 17 / 21 / 25 (.github/workflows/maven-build.yml:25, :33) |
| Compiler API used | javax.tools.ToolProvider.getSystemJavaCompiler() — imported at Compiler.java:64, called at :107 |
| Test surface | src/test/java/io/microsphere/jdk/tools/compiler/CompilerTest.java (11 test methods) |
This module gives you a portable handle on the JDK's own compiler, so one line of code runs in-process
compilation on every JDK from 8 to 25. It is the layer the annotation-processing test stack sits on: it turns
Class objects into source files, runs javac inside your JVM, and hands the work to the annotation processors you
register.
Important
This module does not touch com.sun.tools.javac internals. There is no tools.jar path arithmetic, no
Class.forName("com.sun.tools.javac.…"), no reflection anywhere in its main source. Portability is delegated
entirely to the standard javax.tools API, which the JDK itself adapts per release. Everything in the per-JDK
table below follows from that single call site plus the null check that guards it.
You need this module when a test or tool must compile Java source inside the running JVM rather than shelling out
to javac:
- Annotation-processor tests. Run your processor over real classes and assert on what it produced.
microsphere-annotation-testdoes exactly this:CompilerInvocationInterceptorbuilds aCompiler, feeds it the test's compiled classes as source paths, registers the processor under test plus everyProcessorfound through SPI, and callscompile(...)(microsphere-annotation-test/src/main/java/io/microsphere/test/annotation/processing/CompilerInvocationInterceptor.java:54-63). - Source-to-class generation pipelines — generate a file, compile it, load it.
- "Can this class still be compiled the way I expect?" checks that must run on five different JDKs in CI.
What you get, concretely:
| Capability | Entry point |
|---|---|
| Locate the JDK compiler, fail loudly if absent | new Compiler(...) → Compiler.java:103-109 |
| Compile given classes by their source files | compile(Class<?>...) → Compiler.java:156-167 |
| Register annotation processors | processors(Processor...) → :136-139 |
| Collect diagnostics | diagnosticListener(...) → :141-144 |
| Discover the project root / source dir of any class | detectRootDirectory, detectSourcePath → :264-271, :242-262 |
| Control javac options, locale, encoding | options, locale, charset → :111-114, :146-154 |
The path-discovery statics (detectClassPath / detectRootDirectory / detectSourcePath) are the reason this
module exists at all: javax.tools compiles Files, and the caller normally holds Class objects. Compiler maps
each class to <name>.java (resolveJavaSourceFileRelativePath, Compiler.java:284-286), searches the configured
source paths for it (:217-226), and drops classes whose source it cannot find (:209-215).
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-jdk-tools</artifactId>
<version>0.3.20</version>
</dependency>${revision} is 0.3.20-SNAPSHOT on release (root pom.xml:54); the group id is fixed at root pom.xml:12. The
version is also managed by the reactor BOM (microsphere-java-dependencies/pom.xml:38).
| In-repo consumer | Scope | POM evidence |
|---|---|---|
microsphere-annotation-test |
compile | microsphere-annotation-test/pom.xml:33 |
microsphere-lang-model |
test | microsphere-lang-model/pom.xml:46, <scope>test</scope> at :48 |
microsphere-jdk-tools itself |
compile | microsphere-jdk-tools/pom.xml:14 |
microsphere-annotation-processor has no direct dependency on this module; it reaches it transitively through
microsphere-annotation-test (test scope, microsphere-annotation-processor/pom.xml:53) and through
microsphere-lang-model (microsphere-annotation-processor/pom.xml:33).
Note
microsphere-lang-model declares the dependency but no microsphere-lang-model source or test references
io.microsphere.jdk.tools.compiler.Compiler (verified across the module's sources). Treat that edge as reserved
for tests that drive compilation, not as a runtime requirement.
Transitively you also get microsphere-java-core, whose helpers this module uses heavily — CollectionUtils, Lists,
Sets, SetUtils, FileConstants, SymbolConstants, StringBuilderWriter, SimpleFileScanner, LoggerFactory,
FormatUtils, Assert, ClassUtils, StringUtils (Compiler.java:19-63). Core needs no runtime logging backend;
its facade picks the first available LoggerFactory (microsphere-java-core/src/main/java/io/microsphere/logging/LoggerFactory.java:73-96).
| Item | Value | Evidence |
|---|---|---|
Javadoc @since of Compiler |
1.0.0 |
Compiler.java:70 |
| Release line documented here | release branch, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Published tag | 0.3.20 |
repository tag; group id root pom.xml:12 |
| Bytecode level | not declared in this reactor — inherited from the external parent microsphere-build:0.3.16 (root pom.xml:8-9) |
no maven.compiler.* / <release> property exists anywhere in the module tree |
Warning
Do not quote a -source/-target number for this module. The release branch configures none: the only
maven-compiler-plugin configuration in the whole reactor is -proc:none in
microsphere-annotation-processor, and microsphere-jdk-tools has no <build> section at all
(microsphere-jdk-tools/pom.xml ends at :66). The effective level comes from microsphere-build:0.3.16.
Compiler.java contains no version branch — no if (javaVersion >= 9), no JavaVersion/SystemUtils check, no
Runtime.version() call. Two indirect branches exist and they are the whole story:
getSystemJavaCompiler()(Compiler.java:107) — resolved by the JDK's ownjavax.tools.ToolProvider, which returnsnullwhen no compiler implementation is reachable;Compilerthen throws (:108).Lists.ofList/Sets.ofSet— used byoptions(...)(:112) andprocessors(...)(:137). They bindList.of/Set.ofmethod handles if those methods exist (microsphere-java-core/src/main/java/io/microsphere/collection/Lists.java:101,Sets.java:99) and fall back tounmodifiableList(asList(…))/ a linked-hash set otherwise, inside acatch (Throwable)(Lists.java:520-529,Sets.java:559-568). This changes only the implementation class of the stored options/processors, never the API contract.
| JDK 8 | JDK 11 | JDK 17 | JDK 21 | JDK 25 | |
|---|---|---|---|---|---|
getSystemJavaCompiler() source (Compiler.java:107) |
com.sun.tools.javac.Main from <jdk>/lib/tools.jar, located by ToolProvider |
jdk.compiler module, javax.tools.JavaCompiler service |
same | same | same |
tools.jar |
present in a JDK, absent from a JRE | does not exist | absent | absent | absent |
Does Compiler look for tools.jar? |
No — on every JDK it only null-checks the returned JavaCompiler (:107-108) |
no | no | no | no |
--add-exports / --add-opens needed by this module |
none | none | none | none | none |
| Result on a JRE-only runtime | null → IllegalArgumentException (:108) |
null if jdk.compiler absent from the image (e.g. a jlink image built without it) → same exception |
same | same | same |
options(...) / processors(...) collection impl (:112, :137) |
Arrays.asList-backed unmodifiable list / linked hash set |
List.of/Set.of (immutable) |
immutable | immutable | immutable |
Bytecode produced by compile(...) |
class-file level of the running JDK, because no --source/--release option is ever passed (DEFAULT_OPTIONS, :76-77) |
same | same | same | same |
Source-file discovery (:242-271) |
via CodeSource location, target/classes layout assumed |
same | same | same | same |
The flag that usually circulates for this territory is
--add-exports java.compiler/com.sun.tools.javac.*=ALL-UNNAMED. Two corrections for the release line:
- The package
com.sun.tools.javac.*is not in modulejava.compiler(that module holds the standardjavax.tools/javax.lang.model/javax.annotation.processingAPI). The implementation lives injdk.compiler. microsphere-jdk-toolsnever reads those packages, so it needs no such flag on any JDK in the CI matrix. Flags only become relevant for your code or a harness that castsJavaCompilerdown to acom.sun.tools.javactype.
Tip
CI proves the point: the build command is
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). No --add-opens / --add-exports is passed, and there is
no .mvn/jvm.config in the repository (only .mvn/wrapper/maven-wrapper.properties). So CompilerTest must —
and does — run on a stock JVM on all five JDKs. If your own test needs a module flag, that requirement comes from
your code, not from Compiler.
| Item | Value | Evidence |
|---|---|---|
| Documented line | release, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Newest published release | 0.3.20, tagged 2026-10-16 |
the 0.3.20 tag resolves to chore: merge main into release [skip ci], committed 2026-10-16T03:43:20Z |
| Previous release | 0.3.19, tagged 2026-08-30 |
the 0.3.19 tag, committed 2026-08-30T12:51:37Z |
| Changelog source | release-notes.md at the repository root |
The project publishes no CHANGELOG.md; the v0.3.16–v0.3.20 blocks of release-notes.md read Release notes generation failed and carry raw commit lists, so every entry below is quoted as a commit subject and checked against the release sources. |
0.3.20 adds no new API surface for this area — the 0.3.19...0.3.20 range is a hardening release. What it does change, and how far that reaches the sources this page documents:
- Nullability contracts. The page covers one main source (
jdk/tools/compiler/Compiler.java), and it declares no@Nonnull/@Nullable. - No other change in this range touches the area this page covers.
Note
Reading the commit list: No subject in the range names microsphere-jdk-tools, and its single class declares no nullability annotations — nothing in 0.3.19...0.3.20 is attributable to this module.
All signatures are copied verbatim from release. The type is public class Compiler (Compiler.java:72) with
fluent setters returning this.
public class Compiler {
// Compiler.java:76-77
public static final String[] DEFAULT_OPTIONS = {"-parameters", "-Xlint:-unchecked", "-nowarn", "-Xlint:deprecation"};
// Constructors — Compiler.java:95-109
public Compiler() // source dir + target dir detected from Compiler.class
public Compiler(File targetDirectory) // source dir detected, explicit target
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
// Fluent configuration — all return this
public Compiler options(String... options) // :111-114 REPLACES the list (no append)
public Compiler sourcePaths(File... sourcePaths) // :116-119 APPENDS to the set
public Compiler sourcePaths(Class<?>... sourceClasses) // :121-126 appends each class's detected source path
public Compiler sourcePath(Class<?> sourceClass) // :128-134 no-op when the source cannot be located
public Compiler processors(Processor... processors) // :136-139 REPLACES the set
public Compiler diagnosticListener(DiagnosticListener<? super JavaFileObject> l) // :141-144
public Compiler locale(Locale locale) // :146-149
public Compiler charset(Charset charset) // :151-154
// Terminal operation — Compiler.java:156-167
public boolean compile(Class<?>... sourceClasses) throws IOException;
// Accessors — Compiler.java:169-207
public JavaCompiler getJavaCompiler(); // never null after construction
public StandardJavaFileManager getJavaFileManager() throws IOException; // caller must close
public DiagnosticListener<? super JavaFileObject> getDiagnosticListener(); // null unless set
public Locale getLocale(); // null unless set
public Charset getCharset(); // null unless set
public List<String> getOptions(); // never null; empty when cleared
public Set<Processor> getProcessors(); // never null
// Static discovery — Compiler.java:228-286
public static File detectSourcePath(Class<?> sourceClass); // null if source not found (:242-262)
public static File detectRootDirectory(Class<?> sourceClass); // :264-271
public static File detectClassPath(Class<?> sourceClass); // throws UnsupportedOperationException (:273-282)
public static String resolveJavaSourceFileRelativePath(Class<?> sourceClass); // :284-286
static File defaultSourceDirectory() // package-private, :228-230
static File defaultRootDirectory() // package-private, :232-234
static File defaultTargetDirectory() // package-private, :236-240, mkdirs() with no success check
}| Method | Contract, from the source |
|---|---|
compile(Class...) |
Builds one StandardJavaFileManager per call (:159), sets SOURCE_PATH to the configured source dirs, CLASS_OUTPUT and SOURCE_OUTPUT to the single target dir (:175-177), passes the file manager's Writer capture, calls task.setProcessors(getProcessors()) and returns task.call() (:162-163). true = compilation succeeded. |
compile and missing sources |
Classes whose .java cannot be found are silently dropped — .filter(Objects::nonNull) (:209-215). If all of them are dropped, javac receives an empty compilation unit list and compile normally still returns true. |
getOptions() |
unmodifiableList copy of the configured options; emptyList() when the list is empty (:193-199). |
getProcessors() |
unmodifiableSet, or emptySet() when never set (:201-207). compile() always calls task.setProcessors(getProcessors()) (:162), and javax.tools.CompilationTask runs annotation processing only for a non-empty set — so leaving the field unset (:85) means no processing at all, not "let javac discover processors on the classpath". Register your processors explicitly; that is why CompilerInvocationInterceptor loads them through SPI first (microsphere-annotation-test/.../CompilerInvocationInterceptor.java:60-62). |
getJavaFileManager() |
Not closed by this accessor; compile closes it via try-with-resources (:159), a direct caller must close it too. |
| Nullability | Compiler.java carries no @Nullable / @Nonnull annotations at all (the file's only Javadoc block is the class comment at :66-71); the table above is therefore the contract, read from the code. |
| Thread safety | javaCompiler is final and safely published (:83, :107); every other field is mutable and unsynchronised (:85-93) — see pitfall 7. |
-parameters keeps method parameter names in the class file — which annotation processors and any
reflection-based metadata reader depends on. -Xlint:-unchecked and -nowarn suppress warning output, while
-Xlint:deprecation asks for deprecation notes; the combination is not a contradiction to javac, but it does mean
warnings rarely reach the captured writer (pitfall 4).
Taken from microsphere-jdk-tools/src/test/java/io/microsphere/jdk/tools/compiler/CompilerTest.java — the full
configuration chain plus a compile call:
Compiler compiler = new Compiler();
boolean ok = compiler.compile(Compiler.class, Test.class); // ok == true
compiler.options() // clears 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);Source: CompilerTest.java:143-170 (testCompile). Related assertions:
CompilerTest.java:38-44— the no-arg constructor yields a non-nullJavaCompiler.CompilerTest.java:86-93—detectClassPath(Compiler.class)exists;detectClassPath(String.class)throwsUnsupportedOperationException.CompilerTest.java:107-121—sourcePaths(...)returns the same instance (fluent) and accepts bothFileandClassvarargs.CompilerTest.java:181-187—detectSourcePathreturnsnullfor a class whose source is not in the project tree (assertNull(detectSourcePath(Test.class))).
Note
The comments inside testCompile say "with mocked compiler it's safe to call" (CompilerTest.java:146). Nothing is
mocked in that test — CompilerTest.java:1-28 imports no Mockito type, and mockito-core is only declared
(microsphere-jdk-tools/pom.xml:52-56). The test really runs javac over Compiler.java and CompilerTest.java.
- JDK 8 and
tools.jarare assumed, never detected.CompilercallsToolProvider.getSystemJavaCompiler()(:107) and only checks the result fornull(:108). It never inspectsjava.home, never builds a class loader over<jdk>/lib/tools.jar, and never reports why the compiler is missing. On JDK 8 that means: if you run against a JRE — or a JDK whosetools.jaryour build omitted from a shaded/uber classpath thatToolProvidercannot see — you get one exception with a fixed message and no diagnostic about the missing jar. - The failure is an exception, not silent degradation.
assertNotNullis core's assertion helper and throwsIllegalArgumentException(microsphere-java-core/src/main/java/io/microsphere/util/Assert.java:149-153). Exact message text (Compiler.java:108):No Java compiler available. Ensure this process is running on a JDK (not just a JRE).The rest of the library's convention is to degrade quietly (detectSourcePathreturnsnullat:250-253,searchJavaSourceFilereturnsnullat:225); the constructor is the module's only deliberate fail-fast. Catch it at construction time, not aroundcompile. - JRE versus JDK. A JRE (and a
jlinkimage built withoutjdk.compiler) gives the exception above. Note the asymmetry: the exception arrives when you build aCompiler, so even callingCompiler.detectSourcePath(...)first is safe on a JRE — only the constructors fail. - Compiler output is trace-only.
compile()writes javac's output into aStringBuilderWriter(:158) and discards it; the only place it is surfaced islogger.trace(...)in thefinallyblock (:165). If you pass nodiagnosticListener(...)(the field startsnull,:89) and your logging level is aboveTRACE, a failed compilation returnsfalsewith zero explanation. Either register a listener or enable trace onio.microsphere.jdk.tools.compiler.Compiler. sourcePaths(File...)appends,options(...)andprocessors(...)replace.sourcePathscallsCollectionUtils.addAllinto the existingLinkedHashSet(:117, coreCollectionUtils.java:454), whileoptionsreassigns the field (:112) andprocessorsreassigns too (:137). Callingoptions()with no arguments therefore drops-parameters— silent behaviour change for processors that read parameter names.SOURCE_OUTPUTandCLASS_OUTPUTare both the single target directory (:176-177). Generated sources and generated.classfiles land in the same tree, so acleanthat deletes the target dir also deletes generated sources you might have wanted to inspect.- One instance, many threads is unsupported. Every configuration field is mutable and non-final
(
:79-93). Two threads callingcompile(...)on the same instance while a third reconfigures it can interleave aLinkedHashSetmutation (sourcePaths,:117) with the iteration insidesearchJavaSourceFile(:219) —ConcurrentModificationException. Use oneCompilerper test/compilation, or synchronise externally. - The default target directory is derived from this module's location, not yours.
defaultTargetDirectory()is<root of Compiler.class> /target/generated-classes(:236-240), where the root comes fromdetectRootDirectory(Compiler.class)=classPath.parent.parent(:264-271). In a Maven build that resolves tomicrosphere-jdk-tools/target/generated-classes; when the jar is resolved from your local repository the twogetParentFile()hops land inside the repository layout.dir.mkdirs()is called without checking the result (:238). Pass your owntargetDirectoryif the default matters —new Compiler(new File("src/main/java"), new File("target/generated-classes")). - Missing class path throws.
detectClassPaththrowsUnsupportedOperationExceptionwith the formatted message"The source {} is based on the file system, the class path can't be detected."—{}is core'sFormatUtilsplaceholder (microsphere-java-core/src/main/java/io/microsphere/text/FormatUtils.java:17,:38), replaced by the class — when the class has noCodeSource(:276-281, text at:280). In practice that means classes from the runtime image or from a class loader that reports no code source.detectSourcePathandsourcePath(Class)are the degrading variants (:250-253);detectRootDirectoryand theCompiler()/Compiler(File)constructors are not — they can throw throughdefaultSourceDirectory()→detectSourcePath(null-safe) butdefaultRootDirectory()→detectClassPath(:232-234,:265) throws first. - Source lookup is a recursive, unbounded filesystem scan.
detectSourcePathscans the whole module root withSimpleFileScanner.INSTANCE.scan(rootDirectory, true, …)(:246-247, coreio/scanner/SimpleFileScanner.java:57,:87), matchinggetAbsolutePath().endsWith(relativePath)(:247). On a big tree that is an expensive walk, and a stale copy undertarget/or a generated-sources directory can win the match — the result isfirst(sourceFiles)(:249), whose order comes fromFile#listFiles(). - Windows paths are taken literally.
detectClassPathconverts the code-source URL withnew File(location.getPath())(:277-278), which skips%XXdecoding and URI handling. Project paths with spaces or non-ASCII characters are the cases to test on Windows before you rely on source discovery. -encodingis not in the defaults. Thecharsetfield startsnull(:93), sogetStandardFileManager(...)(:174) uses the JVM default. A source file with non-ASCII literals on a non-UTF-8 default platform charset can change javac's diagnostics or corrupt string constants. Call.charset(UTF_8)(asCompilerTest.java:164does).- Forgetting
.processors(...)silently skips the thing you wanted to test.compile()passesgetProcessors()unconditionally (:162); with the field unset that isemptySet()(:201-207), andCompilationTaskonly runs annotation processing for a non-empty set. The result looks like a successful compile with no generated output and no processor-side assertion firing. - The encoding is supplied as an object, not a name.
getJavaFileManager()handsgetCharset()—public Charset getCharset()(:189-191) — toJavaCompiler.getStandardFileManager(...), whose third argument is the character-encoding name per thejavax.toolscontract; this module contains noCharset→ name adapter between the setter (:151-154) and the call site (:174). The pairing is a type-level inconsistency worth verifying before you depend on non-default encodings; the intended value is the name (getCharset().name()), andCompilerTest.java:164-167is the code that exercises it.
- microsphere-annotation-test — the JUnit 5 extension that drives this module
- microsphere-annotation-processor — the processors most often compiled with it
- microsphere-lang-model — the
javax.lang.modelfacade of the same toolchain - Class Loading and Artifact Detection — User Guide — sibling
JDK-internal-access theme in
core - microsphere-jdk-tools — Developer Guide — line-by-line
Compiler.javaanalysis and diagrams - Chinese version: 中文用户指南