Repository navigation
feat(graalvm): auto-register controller parameter and return types for reflection - #156
Conversation
…r reflection KotowariFeature.registerControllerReflection() now traverses all public controller methods and collects their parameter types and return types, then registers them for reflection automatically. Application-level types (form objects, DTOs, models) no longer need manual entries in reachability-metadata.json. Types in java.*, javax.*, jakarta.*, sun.*, enkan.*, and kotowari.* packages are skipped (covered by built-in analysis or other features). Field types are collected one level deep to handle nested objects. The example app's reachability-metadata.json is reduced to only the two build-time factory entries (NativeApplicationFactory, NativeSystemFactory). Closes #150 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…stration - Fix Javadoc for collectReachableTypes: clarify that recursion is unbounded (cycle-guarded by the result set), not limited to one level - Add jdk.* to shouldSkipType filter alongside existing java.*/sun.* entries - Add recursion test: SimpleForm.Address nested field is collected transitively - Replace HttpRequest (interface) with DefaultHttpRequest (concrete enkan.* class) in exclusion test for clearer intent - Add explicit jdk.* exclusion test using Class.forName to avoid compile-time accessibility issues Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e tests - Make shouldSkipType package-private so it can be tested directly, removing the need for Class.forName on jdk-internal types - Replace jdk.internal.loader.BuiltinClassLoader (implementation-dependent) with jdk.net.ExtendedSocketOptions (stable public API) for jdk.* filter test - Merge redundant collectReachableTypes_includesAppClass into collectReachableTypes_includesAppClassAndFieldTypesTransitively Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances kotowari-graalvm’s KotowariFeature to automatically register controller method parameter/return types (and their reachable field types) for GraalVM reflection, aiming to remove the need for manual reachability-metadata.json entries in apps (including the GraalVM example).
Changes:
- Auto-collect and register controller method parameter and return types for reflection during GraalVM analysis.
- Add unit tests and small test fixtures to validate type collection and skip rules.
- Reduce the GraalVM example’s
reachability-metadata.jsonby removing manual DTO/controller reflection entries.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| kotowari-graalvm/src/main/java/kotowari/graalvm/KotowariFeature.java | Adds reachable-type collection + skip rules and registers collected types for reflection. |
| kotowari-graalvm/src/test/java/kotowari/graalvm/KotowariFeatureTest.java | Adds tests for reachable-type collection and skip behavior. |
| kotowari-graalvm/src/test/java/kotowari/graalvm/controller/SimpleController.java | Adds a method using an app DTO to exercise parameter/return collection. |
| kotowari-graalvm/src/test/java/app/example/SimpleForm.java | New DTO fixture for tests. |
| kotowari-graalvm/src/test/java/app/example/Address.java | Nested DTO fixture for transitive field-type collection. |
| kotowari-graalvm-example/src/main/resources/.../reachability-metadata.json | Removes manual reflection entries relying on new auto-registration behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static boolean shouldSkipType(Class<?> type) { | ||
| String name = type.getName(); | ||
| return name.startsWith("java.") | ||
| || name.startsWith("javax.") | ||
| || name.startsWith("jakarta.") | ||
| || name.startsWith("jdk.") | ||
| || name.startsWith("sun.") | ||
| || name.startsWith("com.sun.") | ||
| || name.startsWith("enkan.") | ||
| || name.startsWith("kotowari."); |
There was a problem hiding this comment.
shouldSkipType currently skips all types under the kotowari. package prefix. This will also skip application/example types that live under kotowari.example.* (e.g. kotowari.example.graalvm.form.TodoForm, kotowari.example.entity.Customer), so controller parameter/return DTOs won’t be auto-registered and the native image will likely fail at runtime. Consider narrowing the skip rule to Kotowari framework packages (e.g. kotowari.graalvm.*, kotowari.routing.*, etc.) or making the skipped prefixes configurable, rather than skipping the entire kotowari. namespace.
| for (Method m : ctrl.getDeclaredMethods()) { | ||
| if (!Modifier.isPublic(m.getModifiers())) continue; | ||
| for (Parameter p : m.getParameters()) { | ||
| collectReachableTypes(p.getType(), appTypes); | ||
| } | ||
| collectReachableTypes(m.getReturnType(), appTypes); | ||
| } |
There was a problem hiding this comment.
This only inspects raw parameter/return types via p.getType() / m.getReturnType(). For signatures like List<Customer> list() or Optional<MyDto> get(), the reachable DTO type(s) are in generic arguments and won’t be collected (e.g. CustomerController.list() in kotowari-example returns List<Customer>). Consider traversing getGenericParameterTypes() / getGenericReturnType() and extracting ParameterizedType arguments (and possibly wildcard bounds) so container return types don’t cause missed reflection registrations.
| @@ -9,27 +9,6 @@ | |||
| "type": "kotowari.example.graalvm.NativeSystemFactory", | |||
| "allDeclaredMethods": true, | |||
| "allDeclaredConstructors": true | |||
There was a problem hiding this comment.
With the current shouldSkipType implementation skipping the entire kotowari.* namespace, the removed reflection entries for kotowari.example.graalvm.{controller/model/form}.* will not be replaced by auto-registration, so the example native build is likely to regress. Either keep these entries until the skip logic is narrowed, or adjust shouldSkipType so application types under kotowari.example.* are included.
| "allDeclaredConstructors": true | |
| "allDeclaredConstructors": true | |
| }, | |
| { | |
| "condition": { | |
| "typeReached": "kotowari.example.graalvm.controller.*" | |
| }, | |
| "type": "kotowari.example.graalvm.controller.*", | |
| "allDeclaredMethods": true, | |
| "allDeclaredConstructors": true | |
| }, | |
| { | |
| "condition": { | |
| "typeReached": "kotowari.example.graalvm.model.*" | |
| }, | |
| "type": "kotowari.example.graalvm.model.*", | |
| "allDeclaredMethods": true, | |
| "allDeclaredConstructors": true | |
| }, | |
| { | |
| "condition": { | |
| "typeReached": "kotowari.example.graalvm.form.*" | |
| }, | |
| "type": "kotowari.example.graalvm.form.*", | |
| "allDeclaredMethods": true, | |
| "allDeclaredConstructors": true |
| import kotowari.graalvm.controller.SimpleController; | ||
| import app.example.Address; | ||
| import app.example.SimpleForm; | ||
| import enkan.data.DefaultHttpRequest; |
There was a problem hiding this comment.
Redundant duplicate import of enkan.data.DefaultHttpRequest (already imported earlier). Please remove the extra import to avoid lint/IDE warnings and keep the import block clean.
| import enkan.data.DefaultHttpRequest; |
- Narrow shouldSkipType from kotowari.* to specific framework subpackages (kotowari.graalvm.*, kotowari.routing.*, kotowari.middleware.*, kotowari.inject.*, kotowari.data.*) so application types under kotowari.example.* are correctly auto-registered - Traverse generic type arguments (ParameterizedType, WildcardType) so container return types like List<Customer> register their type arguments - Remove duplicate import of enkan.data.DefaultHttpRequest in test file - Add tests: generic type argument traversal, kotowari framework package skip, kotowari app package inclusion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use getMethods() instead of getDeclaredMethods() to include action methods inherited from controller superclasses; skip Object methods - Guard getDeclaredFields() traversal with SecurityException catch to handle module-restricted types gracefully - Add Javadoc to shouldSkipType() explaining why only specific kotowari sub-packages are listed and what to do when new ones are added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…egisterAllMethods - Add missing kotowari framework sub-packages to shouldSkipType: kotowari.component.*, kotowari.io.*, kotowari.scope.*, kotowari.system.*, kotowari.util.* - Skip framework-declaring methods early in getMethods() loop to avoid unnecessary type traversal for Validatable/Extendable default methods - Add RuntimeReflection.registerAllMethods(ctrl) alongside registerAllDeclaredMethods so inherited public action methods on controllers are registered for reflection in native image - Expand shouldSkipType test to cover all kotowari framework sub-packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- collectReachableTypes: traverse superclass chain so field types declared in a base class (e.g. BaseForm.address) are collected transitively; each superclass itself is also added to result for reflection registration - appTypes registration: use registerAllFields/registerAllMethods instead of registerAllDeclaredFields/registerAllDeclaredMethods so inherited getters/setters and fields from superclasses are covered - Add BaseForm test fixture; extend SimpleForm from BaseForm so the superclass traversal test is realistic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * <p>Handles raw classes, arrays, parameterized types (e.g. {@code List<Customer>}), | ||
| * and wildcard bounds. Recursively traverses declared field types to handle nested | ||
| * objects (e.g. embedded DTOs). Cycles are prevented by the {@code result} set: once | ||
| * a type is added it is not visited again, so traversal terminates even for | ||
| * mutually-referential types. |
There was a problem hiding this comment.
The PR description says field types are collected “one level deep”, but collectReachableTypes() is fully recursive (it traverses reachable field types transitively). This can significantly increase the set of reflection-registered classes and native image size; either update the PR description to match the actual behavior or add an explicit depth/limit to the traversal to keep it bounded.
| try { | ||
| for (Field f : current.getDeclaredFields()) { | ||
| collectReachableTypes(f.getGenericType(), result); | ||
| } | ||
| } catch (SecurityException e) { | ||
| // Module system may deny field access for some types; skip this class | ||
| } |
There was a problem hiding this comment.
collectReachableTypes() swallows SecurityException when scanning declared fields, which can silently skip nested types and lead to missing reflection registrations at runtime. Consider logging (at least the class name being skipped) or surfacing this in a controlled way so failures are diagnosable during native-image builds.
| /** | ||
| < 10000 td data-line-number="271" class="blob-num blob-num-addition"> | * Returns {@code true} for types that do not need manual reflection registration: | |
| * JDK types, framework types (enkan.* and known kotowari framework sub-packages). | ||
| * | ||
| * <p>Note: only specific kotowari framework sub-packages are listed here rather than | ||
| * the entire {@code kotowari.} namespace, so that application types under | ||
| * {@code kotowari.example.*} or similar are correctly auto-registered. If new | ||
| * framework sub-packages are added to kotowari they must be listed here explicitly. |
There was a problem hiding this comment.
The PR description states that all types under kotowari.* are skipped, but shouldSkipType() intentionally only skips specific kotowari framework sub-packages (and allows application packages like kotowari.example.*). Please update the PR description (or adjust the implementation) so documented behavior matches what the code actually does.
Replace silent swallow with System.err output so native-image build logs show which class triggered the module access denial, aiding diagnosis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
KotowariFeature.registerControllerReflection()now traverses all public controller methods and auto-registers parameter types and return types (including generic type arguments such asList<Customer>) for reflectionjava.*,javax.*,jakarta.*,jdk.*,sun.*,com.sun.*,enkan.*, and specific kotowari framework sub-packages (kotowari.graalvm.*,kotowari.routing.*,kotowari.middleware.*,kotowari.inject.*,kotowari.data.*,kotowari.component.*,kotowari.io.*,kotowari.scope.*,kotowari.system.*,kotowari.util.*) are skipped. Application types underkotowari.example.*or similar are not skipped and are correctly auto-registered.reachability-metadata.jsonis reduced to only the two build-time factory entries (NativeApplicationFactory,NativeSystemFactory) — no more manual form/model/controller entries neededTest plan
collectReachableTypes_includesAppClassAndFieldTypesTransitively— verifiesSimpleForm(extendsBaseForm) collectsBaseFormandAddressvia superclass traversalcollectReachableTypes_traversesGenericTypeArguments— verifiesList<Address>return type collectsAddresscollectReachableTypes_excludesEnkanFrameworkTypes— verifiesenkan.*is excludedshouldSkipType_skipsKotowariFrameworkPackages— verifies all kotowari framework sub-packages are skippedshouldSkipType_doesNotSkipAppClass— verifiesapp.example.*is not skippedKotowariFeatureTesttests still passCloses #150
🤖 Generated with Claude Code