Skip to content

Commit 1180845

Browse files
committed
Refactor TypedPropertyPath into path and PropertyReference.
Allow supporting API that requires operations on a single property and not a property path.
1 parent efb5dbe commit 1180845

File tree

16 files changed

+1163
-50
lines changed

16 files changed

+1163
-50
lines changed

src/jmh/java/org/springframework/data/core/TypedPropertyPathBenchmarks.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import org.junit.platform.commons.annotation.Testable;
1919
import org.openjdk.jmh.annotations.Benchmark;
20-
2120
import org.springframework.data.BenchmarkSettings;
2221

2322
/**
@@ -30,32 +29,32 @@ public class TypedPropertyPathBenchmarks extends BenchmarkSettings {
3029

3130
@Benchmark
3231
public Object benchmarkMethodReference() {
33-
return TypedPropertyPath.of(Person::firstName);
32+
return TypedPropertyPath.ofReference(Person::firstName);
3433
}
3534

3635
@Benchmark
3736
public Object benchmarkComposedMethodReference() {
38-
return TypedPropertyPath.of(Person::address).then(Address::city);
37+
return TypedPropertyPath.ofReference(Person::address).then(Address::city);
3938
}
4039

4140
@Benchmark
4241
public TypedPropertyPath<Person, String> benchmarkLambda() {
43-
return TypedPropertyPath.of(person -> person.firstName());
42+
return TypedPropertyPath.ofReference(person -> person.firstName());
4443
}
4544

4645
@Benchmark
4746
public TypedPropertyPath<Person, String> benchmarkComposedLambda() {
48-
return TypedPropertyPath.of((Person person) -> person.address()).then(address -> address.city());
47+
return TypedPropertyPath.ofReference((Person person) -> person.address()).then(address -> address.city());
4948
}
5049

5150
@Benchmark
5251
public Object dotPath() {
53-
return TypedPropertyPath.of(Person::firstName).toDotPath();
52+
return TypedPropertyPath.ofReference(Person::firstName).toDotPath();
5453
}
5554

5655
@Benchmark
5756
public Object composedDotPath() {
58-
return TypedPropertyPath.of(Person::address).then(Address::city).toDotPath();
57+
return TypedPropertyPath.ofReference(Person::address).then(Address::city).toDotPath();
5958
}
6059

6160
record Person(String firstName, String lastName, Address address) {

src/main/java/org/springframework/data/core/PropertyPath.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* @author Mark Paluch
3131
* @author Mariusz Mączkowski
3232
* @author Johannes Englmeier
33+
* @see PropertyReference
34+
* @see TypedPropertyPath
3335
*/
3436
public interface PropertyPath extends Streamable<PropertyPath> {
3537

@@ -44,7 +46,7 @@ public interface PropertyPath extends Streamable<PropertyPath> {
4446
* @return the typed property path.
4547
* @since 4.1
4648
*/
47-
static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> propertyPath) {
49+
static <T, P> TypedPropertyPath<T, P> of(PropertyReference<T, P> propertyPath) {
4850
return TypedPropertyPaths.of(propertyPath);
4951
}
5052

@@ -60,7 +62,7 @@ static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> propertyPath) {
6062
* @since 4.1
6163
*/
6264
@SuppressWarnings({ "unchecked", "rawtypes" })
63-
static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iterable<P>> propertyPath) {
65+
static <T, P> TypedPropertyPath<T, P> ofMany(PropertyReference<T, ? extends Iterable<P>> propertyPath) {
6466
return (TypedPropertyPath) TypedPropertyPaths.of(propertyPath);
6567
}
6668

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.core;
17+
18+
import java.io.Serializable;
19+
20+
import org.jspecify.annotations.Nullable;
21+
22+
/**
23+
* Interface providing type-safe property references.
24+
* <p>
25+
* This functional interface is typically implemented through method references or lambda expressions that allow for
26+
* compile-time type safety and refactoring support. Instead of string-based property names that are easy to miss when
27+
* changing the domain model, {@code TypedPropertyReference} leverages Java's declarative method references and lambda
28+
* expressions to ensure type-safe property access.
29+
* <p>
30+
* Create a typed property reference using the static factory method {@link #of(PropertyReference)} with a method
31+
* reference or lambda, for example:
32+
*
33+
* <pre class="code">
34+
* TypedPropertyReference&lt;Person, String&gt; name = TypedPropertyReference.of(Person::getName);
35+
* </pre>
36+
*
37+
* The resulting object can be used to obtain the {@link #getName() property name} and to interact with the target
38+
* property. Typed references can be used to compose {@link TypedPropertyPath property paths} to navigate nested object
39+
* structures using {@link #then(PropertyReference)}:
40+
*
41+
* <pre class="code">
42+
* TypedPropertyPath&lt;Person, String&gt; city = TypedPropertyReference.of(Person::getAddress).then(Address::getCity);
43+
* </pre>
44+
* <p>
45+
* The generic type parameters preserve type information across the property path chain: {@code T} represents the owning
46+
* type of the current segment (or the root type for composed paths), while {@code P} represents the property value type
47+
* at this segment. Composition automatically flows type information forward, ensuring that {@code then()} preserves the
48+
* full chain's type safety.
49+
* <p>
50+
* Implement {@code TypedPropertyReference} using method references (strongly recommended) or lambdas that directly
51+
* access a property getter. Constructor references, method calls with parameters, and complex expressions are not
52+
* supported and result in {@link org.springframework.dao.InvalidDataAccessApiUsageException}. Unlike method references,
53+
* introspection of lambda expressions requires bytecode analysis of the declaration site classes and thus depends on
54+
* their availability at runtime.
55+
*
56+
* @param <T> the owning type of this property.
57+
* @param <P> the property value type.
58+
* @author Mark Paluch
59+
* @since 4.1
60+
* @see #then(PropertyReference)
61+
* @see TypedPropertyPath
62+
*/
63+
@FunctionalInterface
64+
public interface PropertyReference<T, P extends @Nullable Object> extends Serializable {
65+
66+
/**
67+
* Syntax sugar to create a {@link PropertyReference} from a method reference or lambda.
68+
* <p>
69+
* This method returns a resolved {@link PropertyReference} by introspecting the given method reference or lambda.
70+
*
71+
* @param property the method reference or lambda.
72+
* @param <T> owning type.
73+
* @param <P> property type.
74+
* @return the typed property reference.
75+
*/
76+
static <T, P extends @Nullable Object> PropertyReference<T, P> of(PropertyReference<T, P> property) {
77+
return PropertyReferences.of(property);
78+
}
79+
80+
/**
81+
* Syntax sugar to create a {@link PropertyReference} from a method reference or lambda for a collection property.
82+
* <p>
83+
* This method returns a resolved {@link PropertyReference} by introspecting the given method reference or lambda.
84+
*
85+
* @param property the method reference or lambda.
86+
* @param <T> owning type.
87+
* @param <P> property type.
88+
* @return the typed property reference.
89+
*/
90+
static <T, P> PropertyReference<T, P> ofMany(PropertyReference<T, ? extends Iterable<P>> property) {
91+
return (PropertyReference) PropertyReferences.of(property);
92+
}
93+
94+
/**
95+
* Get the property value for the given object.
96+
*
97+
* @param obj the object to get the property value from.
98+
* @return the property value.
99+
*/
100+
@Nullable
101+
P get(T obj);
102+
103+
/**
104+
* Returns the owning type of the referenced property..
105+
*
106+
* @return the owningType will never be {@literal null}.
107+
*/
108+
default TypeInformation<?> getOwningType() {
109+
return PropertyReferences.getMetadata(this).owner();
110+
}
111+
112+
/**
113+
* Returns the name of the property.
114+
*
115+
* @return the current property name.
116+
*/
117+
default String getName() {
118+
return PropertyReferences.getMetadata(this).property();
119+
}
120+
121+
/**
122+
* Returns the actual type of the property at this segment. Will return the plain resolved type for simple properties,
123+
* the component type for any {@link Iterable} or the value type of {@link java.util.Map} properties.
124+
*
125+
* @return the actual type of the property.
126+
* @see #getTypeInformation()
127+
* @see TypeInformation#getRequiredActualType()
128+
*/
129+
default Class<?> getType() {
130+
return getTypeInformation().getRequiredActualType().getType();
131+
}
132+
133+
/**
134+
* Returns the type information for the property at this segment.
135+
*
136+
* @return the type information for the property at this segment.
137+
*/
138+
default TypeInformation<?> getTypeInformation() {
139+
return PropertyReferences.getMetadata(this).propertyType();
140+
}
141+
142+
/**
143+
* Returns whether the property is a collection.
144+
*
145+
* @return {@literal true} if the property is a collection.
146+
* @see #getTypeInformation()
147+
* @see TypeInformation#isCollectionLike()
148+
*/
149+
default boolean isCollection() {
150+
return getTypeInformation().isCollectionLike();
151+
}
152+
153+
/**
154+
* Extend the property to a property path by appending the {@code next} path segment and return a new property path
155+
* instance.
156+
*
157+
* @param next the next property path segment as method reference or lambda accepting the owner object {@code P} type
158+
* and returning {@code N} as result of accessing a property.
159+
* @param <N> the new property value type.
160+
* @return a new composed {@code TypedPropertyPath}.
161+
*/
162+
default <N extends @Nullable Object> TypedPropertyPath<T, N> then(PropertyReference<P, N> next) {
163+
return TypedPropertyPaths.compose(this, next);
164+
}
165+
166+
/**
167+
* Extend the property to a property path by appending the {@code next} path segment and return a new property path
168+
* instance.
169+
*
170+
* @param next the next property path segment as method reference or lambda accepting the owner object {@code P} type
171+
* and returning {@code N} as result of accessing a property.
172+
* @param <N> the new property value type.
173+
* @return a new composed {@code TypedPropertyPath}.
174+
*/
175+
default <N extends @Nullable Object> TypedPropertyPath<T, N> thenMany(
176+
PropertyReference<P, ? extends Iterable<N>> next) {
177+
return (TypedPropertyPath) TypedPropertyPaths.compose(this, next);
178+
}
179+
180+
}

0 commit comments

Comments
 (0)