Changes to support Micronaut processors as a Native Image#12449
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Micronaut’s annotation processing/visitor service loading to better support execution in native-image contexts (e.g., by using an alternative classloader strategy and a ServiceLoader fallback), and enhances SoftServiceLoader instantiation behavior with a MethodHandle-first approach and reflection fallback.
Changes:
- Add optional context-classloader-based service loading (behind
micronaut.processing.use.context.classloader) and JDKServiceLoaderfallbacks for visitors/mappers/transformers. - Make
SoftServiceLoaderinstantiate services via MethodHandles first, with reflection fallback when access via MethodHandles is not possible. - Add a regression test covering reflection fallback when MethodHandles cannot access a constructor.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java | Adds context classloader selection and a ServiceLoader fallback for TypeElementVisitor discovery; adjusts supported-options detection and deduping. |
| core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java | Introduces shared instantiation helper using MethodHandles first with reflection fallback. |
| core/src/test/java/io/micronaut/core/io/service/SoftServiceLoaderTest.java | Adds test ensuring reflection fallback is used when MethodHandles cannot access a constructor. |
| core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java | Adds context classloader selection and ServiceLoader fallback for BeanElementVisitor loading. |
| core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java | Switches mapper/transformer/remapper/validator loading to use context classloader selection and ServiceLoader fallback. |
|
@copilot create a PR that targets this PR and addresses you comments |
|
@graemerocher I've opened a new pull request, #12451, to work on those changes. Once the pull request is ready, I'll request review from you. |
…zy first-service loading, handle InaccessibleObjectException in reflection fallback, preserve order in visitor dedup (#12451) * Initial plan * Address PR review comments: centralize property constant, lazy first-service load, InaccessibleObjectException handling, LinkedHashMap for dedup Co-authored-by: graemerocher <66626+graemerocher@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: graemerocher <66626+graemerocher@users.noreply.github.com>
|
@copilot create a new PR addressing the comments |
|
@graemerocher I've opened a new pull request, #12453, to work on those changes. Once the pull request is ready, I'll request review from you. |
… transparency in service loading (#12453) * Address review feedback: iterator-based ServiceLoader fallbacks, preserve exception cause, update StaticDefinition Javadoc Co-authored-by: graemerocher <66626+graemerocher@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: graemerocher <66626+graemerocher@users.noreply.github.com>
| def previous = Thread.currentThread().contextClassLoader | ||
| def propertyName = "micronaut.processing.use.context.classloader" | ||
| def previousProperty = System.getProperty(propertyName) | ||
|
|
There was a problem hiding this comment.
This test hard-codes the system property name ("micronaut.processing.use.context.classloader") even though the code introduces VisitorContext.MICRONAUT_PROCESSING_USE_CONTEXT_CLASSLOADER. Using the constant here would prevent drift if the option key changes in the future.
| for (ServiceDefinition<S> definition : SoftServiceLoader.load(serviceType, classLoader).disableFork()) { | ||
| try { | ||
| if (definition.isPresent()) { | ||
| return Optional.of(definition.load()); | ||
| } | ||
| } catch (Throwable e) { | ||
| if (e instanceof VirtualMachineError virtualMachineError) { | ||
| throw virtualMachineError; | ||
| } | ||
| } | ||
| } | ||
| Iterator<ServiceLoader.Provider<S>> it = ServiceLoader.load(serviceType, classLoader).stream().iterator(); | ||
| while (it.hasNext()) { | ||
| try { | ||
| return Optional.of(it.next().get()); | ||
| } catch (Throwable e) { | ||
| if (e instanceof VirtualMachineError virtualMachineError) { | ||
| throw virtualMachineError; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
loadFirstService catches and ignores any throwable from ServiceDefinition#load() / provider creation (except VirtualMachineError). Previously, a failing first AnnotatedElementValidator would surface immediately; now it can be silently skipped, potentially disabling validation with no signal. Consider only ignoring known “soft” failures (e.g. missing classes) and otherwise rethrowing or at least recording/reporting the error.
| Iterator<ServiceLoader.Provider<BeanElementVisitor>> iterator = ServiceLoader.load(BeanElementVisitor.class, classLoader).stream().iterator(); | ||
| while (iterator.hasNext()) { | ||
| try { | ||
| BeanElementVisitor<?> visitor = iterator.next().get(); | ||
| if (visitor.isEnabled()) { | ||
| visitors.add(visitor); | ||
| } | ||
| } catch (Throwable e) { | ||
| if (e instanceof VirtualMachineError virtualMachineError) { | ||
| throw virtualMachineError; | ||
| } | ||
| } |
There was a problem hiding this comment.
The ServiceLoader fallback loop swallows all throwables (except VirtualMachineError). If a BeanElementVisitor is present but misconfigured (e.g. ServiceConfigurationError) this will be silently ignored and visitor execution may be partially/entirely disabled without any indication. Consider rethrowing ServiceConfigurationError/LinkageError (or otherwise surfacing the failure) to avoid masking real configuration issues.
|


Summary
micronautcprerequisite changes from micronautc: custom javac native image build with annotation processors embedded #12444 so they can be reviewed and merged independentlySoftServiceLoaderwith reflection fallback and coverage inSoftServiceLoaderTestVerification
./gradlew :micronaut-core:test --tests 'io.micronaut.core.io.service.SoftServiceLoaderTest' :micronaut-core-processor:compileJava :micronaut-inject-java:compileJava