The Scope of "No Reflection"

The principle applies to the framework core: routing, parameter extraction, and JSON handling use no reflection anywhere.

The default template engine sits right at the boundary, so its scope is worth stating exactly. jte compiles every template to a Java class, so ${deck.title} runs as a compiled method call. The model is never reflected over, and a typo in that expression is a compile error rather than a silently empty value. What remains is loading: the engine looks the generated class up by name and invokes its render method reflectively. That reflection is confined to classes the build generated, and never reaches a class you wrote. That confinement is what keeps the native image story mechanical: jte’s own build extension writes the reflection config for exactly those generated classes, and neither a model class nor a framework class needs an entry.

A template engine in a module of its own answers for itself: Handlebars falls back to reflection for a record or a bean, FreeMarker reads one through its object wrapper, and Thymeleaf evaluates every expression through OGNL. That is one more reason each lives outside core. Take the module and you have taken its reflection with it, and an application that stays on the default jte keeps its model access reflection-free.

spider-silk-jetty-websocket answers for itself the same way, and answers well. Jetty binds an endpoint’s callbacks by taking a MethodHandle to each, cached per endpoint class, and every connection the module opens is one class it declares itself. That is the one class Jetty looks up. A WebSocketHandler an application writes is reached through an interface call and is never reflected over.

A JSON binding library an application adds answers for itself on the same terms as a template module: take it and you have taken its reflection with it. Jackson and Gson read and write fields reflectively, so every bound type needs an entry in a native image config. avaje-jsonb generates an adapter per type at compile time and needs none, which is the same trade jte makes for templates. Core is not involved either way: the seam is WebResponse.json(String) and req.bodyStream(), and an application that stays on the hand-written writers keeps its wire format reflection-free.

The example project’s repositories choose spring-jdbc’s DataClassRowMapper, which constructs the domain records reflectively. That is the example’s choice and not the framework’s, kept deliberately to show where the boundary runs. Under a native image that choice is paid for with a reflection config confined to the one domain package, generated by the build. Core contributes not a single entry to it.