Reflection — Class Mapping, Mixins, and Proxy Support¶
Overview and Motivation¶
org.tentackle.reflect is where the framework's dynamic behavior is implemented. Three of
Tentackle's most distinctive features are not language features — they are reflection, done
carefully and cached:
- PDOs emulate multiple inheritance. A
PersistentDomainObjectis a dynamic proxy whose calls are routed to two delegates, a persistence one and a domain one. TheMixinis what makes that work. - Implementations are found, not wired.
@MappedServicesays "this class implements that interface"; theClassMapperis the runtime engine that resolves it — including up class hierarchies and across packages. - Immutable objects can be deserialized. An object with no no-arg constructor and final fields
cannot be reconstructed the usual way. The
@CanonicalConstructorfamily tells TRIP how to build it.
The package also holds the interception machinery. That half is documented in depth in interceptors.md — this document covers the rest and explains only how interception plugs into the proxy support described here.
A word on the design: reflection is slow if you let it be. Nearly every type in this package exists
to do a reflective lookup once and cache it — ConcurrentHashMaps of resolved mappings,
constructors, method indexes, and property accessors are the norm. The expensive work happens at
class-load or first-use time; the steady state is a hash lookup.
Class Mapping¶
ClassMapper — the engine behind @MappedService¶
The service API has two flavors:
@Service locates a singleton, @MappedService maps implementations to the interfaces they
implement. The build-time tooling writes those relationships into META-INF; at runtime,
ClassMapper resolves them.
A mapper is created per mapped service type and given a name used in logs and error messages:
ClassMapper mapper = ClassMapper.create("FX table-config-provider", TableConfigurationProvider.class);
Class<?> providerClass = mapper.mapLenient(rowClass);
The interface is small — map and mapLenient, each taking a Class or a name, plus a default
class and listeners — but the lenient variant carries the weight:
| Method | Lookup |
|---|---|
map(Class \| String) |
The explicit mapping only. Throws ClassNotFoundException if there is none. |
mapLenient(Class \| String) |
Walks up the superclass chain (all the way to Object), then tries the interfaces, recursively through super-interfaces. |
Lenient mapping is why a provider registered for a base class or an interface serves every subclass
without further registration — one GuiProvider for an entity hierarchy, one TableCellType for
PersistentDomainObject covering every PDO. Note the asymmetry, which the javadoc calls out: when
mapping a class, its interfaces are ignored until the superclass chain is exhausted.
setDefaultClass(...) supplies a fallback for unmapped classes instead of an exception, and
ClassMappedListeners fire the first time
a class is mapped.
The base implementation AbstractClassMapper
provides the lenient and package/basename lookup and caches every resolved name in a
ConcurrentHashMap, so the hierarchy walk happens once per class. Resolution is logged at FINE
(configurable per mapper) — the log line <mapper>: added <from> -> <to> is the first place to look
when an implementation isn't found.
DefaultClassMapper— initialized from aMapor aPropertiesobject (a file, XML, or built at runtime).ClassToServicesMapper— maps a class to all matching services rather than the first, for cases where every implementation on the path should contribute.ClassMapperFactory— the replaceable singleton behindClassMapper.create(...).
Consumers are everywhere: PdoInvocationHandler (persistence and domain implementations), the TRIP
factories, and the FX factories for GuiProviders, table configurations, cell types and context
menus.
Proxies and Mixins¶
Mixin — emulating multiple inheritance¶
Java has single inheritance. A PDO needs two implementations — persistence and domain — behind one
interface. Mixin<T,M> is the piece that closes the gap.
A mixin binds three things: the declaring interface, the implementation class found through
a ClassMapper, and the delegate instance it creates. Given a PDO interface, the constructor
maps leniently to the implementation, then identifies which of that implementation's interfaces
extends the expected super-interface — that becomes the declaring class, and it is how the invocation
handler later decides whether a method belongs to this mixin.
PdoInvocationHandler
holds exactly two:
private final Mixin<T,PersistentObject<T>> persistenceMixin;
private final Mixin<T,DomainObject<T>> domainMixin;
...
persistenceMixin = new Mixin<>(persistenceMapper, clazz, PersistentObject.class);
domainMixin = new Mixin<>(domainMapper, clazz, DomainObject.class);
Each call on the proxy is routed to the delegate whose declaring interface declares the method. The result is an object that is both its persistence and its domain implementation — see pdo.md for what that buys you and modules.md for why the two implementations live in separate modules that never see each other.
Mixins cache aggressively: constructors are looked up once per (implementation class, parameter
types) pair, and a static match cache remembers, per class pair, whether a class implements a given
declaring interface. @TripReferencable(NEVER) marks them as never referenced on the wire — the
proxy is what travels, not its mixins.
DummyDelegate is a marker letting a mixin with
a real delegate class be treated as a dummy — used where a proxy needs the shape but not the
behavior.
EffectiveClassProvider — what getClass() can't tell you¶
Call getClass() on a dynamic proxy and you get the proxy's synthetic class, which is almost never
what you want. EffectiveClassProvider<T>
is the interface an object implements to report its effective class — the one the application
thinks in terms of.
This matters more than it sounds: validation, scripting, the FX table configuration and the security
interceptors all key off the effective class, and all of them would silently misbehave against a
proxy class name. Implementors include AbstractDomainObject, AbstractDomainOperation,
ValidationUtilities, ScriptVariable, DefaultTableConfiguration, and the security interceptors.
Interception¶
The interception types — Interceptable,
@Interception,
Interceptor,
AbstractInterceptor,
InterceptableMethod,
InterceptableMethodCache and
InterceptableInvocationHandler
— are covered by interceptors.md: the annotation model, the proceed
contract, chaining and order, the built-in interceptors, and compile-time verification.
What belongs here is how they connect to the machinery above. Interception is layered onto the same
dynamic proxies the mixins feed:
InterceptableFactory (with
DefaultInterceptableFactory)
creates interceptable objects;
@InterceptionService is a
@MappedService for Interceptable, so implementations are located through a ClassMapper exactly
like any other mapped service;
InterceptionUtilities is the
replaceable @Service holding the helper logic; and
InterceptableMethodInvoker — one
per interceptable type (PDO, operation, …) — performs the invocation and extends MethodStatistics,
which is where per-method invocation counts and timings come from (see logging.md).
Canonical Construction¶
An immutable object — final fields, no setters, no no-arg constructor — is a problem for any
deserializer: there is no way to create it empty and fill it in. Java records solve this with a
canonical constructor the compiler knows about. Tentackle generalizes the idea to ordinary classes
with a small annotation family, which is how TRIP reconstructs immutable values without
requiring -parameters at compile time or resorting to Unsafe.
| Annotation | Applies to | Meaning |
|---|---|---|
@CanonicalConstructor |
constructor | This is the constructor to build the object with |
@CanonicalName |
parameter | The parameter's name, since reflection can't recover it without -parameters |
@CanonicalField |
field | This field supplies a constructor argument |
@CanonicalGetter |
method | This public no-arg getter supplies a constructor argument |
@CanonicalBuilder |
type or constructor | The object is built through a builder instead |
The matching of constructor parameters to members is by name: either annotate the parameters
with @CanonicalName, or annotate the members with @CanonicalField / @CanonicalGetter. The
explicit-name requirement is a deliberate trade — Tentackle does not want to force every application
to compile with javac -parameters.
@CanonicalBuilder covers the builder pattern: the constructor takes a single builder argument; the
builder has a no-arg constructor, one method per field named after the field, taking the value and
returning the builder, plus a no-arg method returning the finished object. The
DTO wurblet's --canonical option
generates exactly this shape.
ObjectTripType reads all of it through
ReflectionHelper.getCanonicalConstructor(...) / getCanonicalBuilderInfo(...) and switches to
delayed construction: components are deserialized first, then the object is built in one shot.
Without a canonical constructor or a no-arg constructor, a class is simply not serializable by TRIP
— which is why trip.md recommends adding one as the first remedy.
Caches, Visitors, and Helpers¶
Method caches¶
MethodCache— a thread-safe cache for method lookups that involve a best-match search, which is expensive to repeat.ReflectiveVisitoris the motivating case.IndexedMethodCache— one per class, mapping methods to small integer indexes and back. This is what lets TRIP name a method on the wire with anintrather than a signature string:DefaultRemoteDelegateandDefaultRegistryassign an index on first use and both sides then speak in indexes.
ReflectiveVisitor¶
ReflectiveVisitor implements the visitor
pattern without the accept-method boilerplate: the visit method for a concrete class is found by
reflection, choosing the most specific match, so one method can serve a whole class hierarchy
and a more specific one overrides it for a subclass. It backs
PersistenceVisitor,
which is how applications add behavior across entity types without touching the entities — see
database.md.
PropertyMap¶
PropertyMap<T> exposes a bean's properties as a
Map<String,Object>, with PropertyMapping<T>
describing each one — get/set or is/set pairs, or a read-only method. AbstractBuilder in
tentackle-fx uses it to configure components
generically by property name.
ReflectionHelper¶
ReflectionHelper is the static toolbox the
rest of the framework reaches for. Beyond the canonical-construction lookups already mentioned:
| Area | Methods |
|---|---|
| Names | getClassBaseName, getPackageName, getPackagePathName, getOutermostClassName, getSerializableClassName, methodToString, makeDeclareString |
| Loading | loadClass |
| Primitives | primitiveToWrapperClass, wrapperToPrimitiveClass |
| Type tests | isAssignableFrom, isAnnotationPresent (both with all/any semantics), isNonStaticInnerClass, isImmutableCollection |
| Generics | extractGenericInnerTypeClass |
| Members | getAllFields, getAllMethods (walking the hierarchy) |
| Bean conventions | getGetterName, getFieldName, getGetter, getSetter, isGetter, isSetter |
The small details matter here: it knows about synthetic this$0 parameters of inner classes and
about java.util.ImmutableCollections, both of which trip up naive reflection over serialized
graphs.
Package Layout¶
| Group | Types |
|---|---|
| Class mapping | ClassMapper, AbstractClassMapper, DefaultClassMapper, ClassMapperFactory, DefaultClassMapperFactory, ClassToServicesMapper, ClassMappedListener |
| Proxies & mixins | Mixin, DummyDelegate, EffectiveClassProvider |
| Interception | Interceptable, Interception, Interceptor, AbstractInterceptor, InterceptableMethod, InterceptableMethodCache, InterceptableMethodInvoker, InterceptableInvocationHandler, InterceptableFactory, DefaultInterceptableFactory, InterceptionService, InterceptionUtilities → interceptors.md |
| Canonical construction | CanonicalConstructor, CanonicalName, CanonicalField, CanonicalGetter, CanonicalBuilder |
| Caches | MethodCache, IndexedMethodCache |
| Helpers | ReflectionHelper, ReflectiveVisitor, PropertyMap, PropertyMapping |
Related Documentation¶
- Tentackle Core — the runtime foundation this package belongs to.
- Interceptors — the interception model in depth.
- PDO — the dynamic proxy the mixins build.
- Service and Configuration API —
@Serviceand@MappedService, whichClassMapperresolves. - TRIP — canonical construction and indexed method calls in practice.
- Wurblets — the DTO wurblet's
--canonicaloption. - Miscellaneous — the sibling utility package.