-
Notifications
You must be signed in to change notification settings - Fork 700
Description
When it comes to referencing properties of domain types, the predominant approach across most of our component methods has traditionally relied upon String-based property references. A pattern that, while pragmatic in its simplicity, exposes the codebase to an entire taxonomy of defects by design: Typographical errors, stale references persisting after refactoring operations, and the general brittleness inherent to stringly-typed APIs. Static analysis of such references requires involved approaches that do not guarantee immediate discovery.
Certain components within our modules have indeed embraced model-based query expression facilities (Querydsl and the JPA Criteria API metamodel).
However, adoption of these necessitates non-trivial build process customization alongside the introduction of additional library dependencies. Generated metamodels provide compile-time validation, yet they fall short during refactoring scenarios lacking synchronization between model classes and their underlying domain entities.
Our earlier MethodInvocationRecorder facility represented an initial attempt addressing this deficiency. MethodInvocationRecorder creates proxy instances (typically CGlib-based proxies). Proxies have fundamental limitations when confronted with final classes, sealed class hierarchies, and Kotlin's design philosophy where extensibility is opt-in rather than default. In these arrangements, proxy-based subclassing simply isn't viable.
What we're seeking is a natural programming model for expressing property references that aligns with modern Java and Kotlin idioms. Drawing inspiration from Function<T, R> and the method reference syntax introduced in Java 8, we observe that method references provide a concise, declarative, and type-safe mechanism. By constraining our focus specifically to JavaBeans-style getter methods, we can express property references elegantly as:
Person::getNameIn the context of our PropertyPath abstraction, this would embrace method references as first-class citizens:
PropertyPath.from("name", Person.class) // existing String-based API
PropertyPath.of(Person::getName) // type-safe property reference expression
PropertyPath.from("address.country", Person.class) // existing nested path API
PropertyPath.of(Person::getAddress).then(Address::getCountry) // type-safe composed path expressionThe benefits of type-associated method references extend naturally to other framework components. Consider Sort, where type-safe property references dramatically enhance both readability and type safety, while enabling the introduction of compile-time constraints that enforce type coherence:
Sort.by(Person::getFirstName, Person::getLastName)
// as opposed to the stringly-typed alternative:
Sort.by("firstName", "lastName");
Sort.by(Person::getFirstName, Order::getOrderDate)
// fails at compile-time with: Type parameter T has incompatible upper bounds: Person and OrderThis approach harmonizes with Java's type system while maintaining simplicity and directness.
Kotlin provides first-class support for property references, allowing direct property access through Person::firstName and Person::lastName rather than traditional getter-based reflective references. Our current Kotlin extensions build upon this foundation, exposing a KProperty<*>.toDotPath extension method:
(Person::address / Address::country).toDotPath()While this approach delivers the type safety we'd expect, it introduces repeated dot-path resolution calls at the application code level.
We can do better by embracing property references as first-class citizen creating a unified property path representation that naturally expresses type-safe traversal semantics:
(Person::address).toDotPath() // existing String-based API variant
PropertyPath.of(Person::address) // type-safe property reference expression
(Person::address / Address::city).toDotPath() // existing String-based API variant
PropertyPath.of(Person::address / Address::city) // type-safe composed path expression
// composition with explicit generics
PropertyPath.of<Person, Address>(Person::address)
.then<Country>(Address::country)This evolution presents a natural opportunity to revisit and deprecate our org.springframework.data.mapping Kotlin extensions in favor of a consolidated abstraction that serves Java and Kotlin consumers equally well without compromising on developer experience or introducing unnecessary ceremony.