Repository navigation
feat(graalvm): auto-register all META-INF/services SPI implementations for reflection - #159
Conversation
…r reflection KotowariFeature now scans every META-INF/services/<name> file on the application classpath at native-image build time and registers each discovered implementation class for reflection automatically. Previously, any MessageBodyReader/Writer added via ServiceLoader required manual reflect-config.json entries to avoid silent runtime failures. The fix is generic: all SPI implementations on the classpath are covered, not just SerDes types. Key design decisions: - Uses access.getApplicationClassPath() (List<Path>) for reliable classpath scanning — avoids ClassLoader.getResources() which silently fails for JARs without directory entries - Handles FileSystemAlreadyExistsException: reuses the existing FileSystem rather than opening a duplicate - Scans only root-level META-INF/services/ files; skips multi-release JAR subdirs (META-INF/versions/N/...) - Filtering of framework/JDK types is done in registerServiceLoaderImpls via shouldSkipType(), keeping discoverServiceLoaderClasses() a pure testable helper that returns all resolved classes Closes #151 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reorder imports to alphabetical (java.io before java.lang) - Add comment explaining why openedHere guards the fs.close() call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds GraalVM native-image build-time auto-registration of Service Provider Interface (SPI) implementations found under META-INF/services/, so ServiceLoader-discovered providers (e.g., JAX-RS MessageBodyReader/Writer) don’t require manual reflect-config.json entries.
Changes:
- Scan every application classpath entry for
META-INF/services/*files and resolve listed provider classes. - Register discovered provider implementation classes for reflection during
beforeAnalysis. - Add unit tests for service-file discovery (directory entry + missing-class handling).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| kotowari-graalvm/src/main/java/kotowari/graalvm/KotowariFeature.java | Implements classpath scanning of META-INF/services/* and registers discovered provider implementations for reflection. |
| kotowari-graalvm/src/test/java/kotowari/graalvm/KotowariFeatureTest.java | Adds tests validating service provider discovery from a directory and skipping unresolvable providers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| line = line.strip(); | ||
| if (line.isEmpty() || line.startsWith("#")) continue; |
There was a problem hiding this comment.
parseServiceFile only treats lines starting with # as comments, but ServiceLoader provider-configuration syntax allows inline comments (e.g. com.Foo # optional). Without stripping the trailing #... portion, Class.forName will fail and valid providers won’t be registered. Consider truncating each non-empty line at the first # before stripping/processing.
| line = line.strip(); | |
| if (line.isEmpty() || line.startsWith("#")) continue; | |
| int commentStart = line.indexOf('#'); | |
| if (commentStart >= 0) { | |
| line = line.substring(0, commentStart); | |
| } | |
| line = line.strip(); | |
| if (line.isEmpty()) continue; |
| RuntimeReflection.registerAllDeclaredFields(cls); | ||
| RuntimeReflection.registerAllDeclaredMethods(cls); |
There was a problem hiding this comment.
registerServiceLoaderImpls registers all declared fields and methods for every discovered SPI implementation. This can significantly increase native-image reflection metadata (startup/memory/image size) and broadens reflective access beyond what ServiceLoader typically needs (constructors). Consider narrowing registration to constructors only (or constructors + public members actually used reflectively) unless there’s a concrete need for full member registration.
| RuntimeReflection.registerAllDeclaredFields(cls); | |
| RuntimeReflection.registerAllDeclaredMethods(cls); |
| URL classesUrl = SimpleForm.class.getProtectionDomain().getCodeSource().getLocation(); | ||
| URLClassLoader cl = new URLClassLoader(new URL[]{classesUrl}, null); | ||
|
|
||
| Path tmpDir = Files.createTempDirectory("spi-dir-test"); |
There was a problem hiding this comment.
This test manually closes URLClassLoader cl at the end; if an assertion fails earlier, the classloader won’t be closed. Prefer try-with-resources for URLClassLoader (consistent with shouldSkipType_doesNotSkipUnnamedModuleAppClass) and consider using JUnit’s @TempDir or cleanup to avoid leaving temp directories behind.
| URLClassLoader cl = new URLClassLoader( | ||
| new URL[]{tmpDir.toUri().toURL()}, getClass().getClassLoader()); | ||
|
|
||
| assertThatCode(() -> feature.discoverServiceLoaderClasses(List.of(tmpDir), cl)) | ||
| .doesNotThrowAnyException(); | ||
| cl.close(); |
There was a problem hiding this comment.
This test manually calls cl.close(); if discoverServiceLoaderClasses(...) throws unexpectedly (or an assertion is added later), the classloader may leak. Use try-with-resources for the URLClassLoader and consider @TempDir for the temp directory to ensure cleanup.
- parseServiceFile: strip inline comments per ServiceLoader spec (e.g. "com.Foo # note" is valid and was previously causing ClassNotFoundException) - Tests: use try-with-resources for URLClassLoader to ensure close on assertion failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
KotowariFeaturenow scans everyMETA-INF/services/<name>file on the application classpath at native-image build time and registers each discovered implementation class for reflection automaticallyMessageBodyReader/MessageBodyWriteradded viaServiceLoaderno longer requires manualreflect-config.jsonentriesDesign decisions
access.getApplicationClassPath()(List<Path>) for reliable scanning — avoidsClassLoader.getResources("META-INF/services/")which silently fails for JARs without directory entriesFileSystemAlreadyExistsException: reuses the existingFileSystemrather than opening a duplicate (GraalVM may have already opened the JAR)META-INF/services/files; skipsMETA-INF/versions/N/subdirs in multi-release JARsshouldSkipType()) is applied inregisterServiceLoaderImpls(), keepingdiscoverServiceLoaderClasses()a pure, independently testable helperTest plan
discoverServiceLoaderClasses_findsImplFromDirectoryEntry— discovers an impl registered in a temp directory services filediscoverServiceLoaderClasses_ignoresMissingClass— silently skips unresolvable class names without throwingmvn test -pl kotowari-graalvm -am -DexcludedGroups=integrationCloses #151
🤖 Generated with Claude Code