Skip to content

Commit 6e91c1f

Browse files
committed
Documentation.
1 parent 1fcd7f1 commit 6e91c1f

File tree

8 files changed

+503
-295
lines changed

8 files changed

+503
-295
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[[type-safe-property-references]]
2+
= Type-safe Property References
3+
4+
Type-safe property references address a common source of friction in data access code: The reliance on literal, dot-separated property path strings.
5+
Such stringly-typed references are fragile during refactoring difficult to identify as they often lack context.
6+
Type-safe property paths favor explicitness and compiler validation by deriving property paths from Java method references.
7+
8+
A property path is a simple, transportable representation of object navigation.
9+
When expressed as a method-reference the compiler participates in validation and IDEs provide meaningful refactoring support.
10+
The result is code that reads naturally, fails fast on renames, and integrates cleanly with existing query and sorting abstractions, for example:
11+
12+
[source,java]
13+
----
14+
TypedPropertyPath.of(Person::getAddress)
15+
.then(Address::getCity);
16+
----
17+
18+
The expression above constructs a path equivalent to `address.city` while remaining resilient to refactoring.
19+
Property resolution is performed by inspecting the supplied method references; any mismatch becomes visible at compile time.
20+
21+
By comparing a literal-based approach as the following example you can immediately spot the same intent while the mechanism of using strings removes any type context:
22+
23+
.Stringly-typed programming
24+
[source,java]
25+
----
26+
Sort.by("address.city", "address.street")
27+
----
28+
29+
You can also use it inline for operations like sorting:
30+
31+
.Type-safe Property Path
32+
[source,java]
33+
----
34+
Sort.by(Person::getFirstName, Person::getLastName);
35+
----
36+
37+
`TypedPropertyPath` can integrate seamlessly with query abstractions or criteria builders:
38+
39+
.Type-safe Property Path
40+
[source,java]
41+
----
42+
Criteria.where(Person::getAddress)
43+
.then(Address::getCity)
44+
.is("New York");
45+
----
46+
47+
Adopting type-safe property references aligns with modern Spring development principles.
48+
Providing declarative, type-safe, and fluent APIs leads to simpler reasoning about data access eliminating an entire category of potential bugs through IDE refactoring support and early feedback on invalid properties by the compiler.
49+
50+
Lambda introspection is cached for efficiency enabling repeatable use.
51+
The JVM reuses static lambda instances contributing to minimal overhead of one-time parsing.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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.lang.invoke.SerializedLambda;
19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Member;
21+
import java.lang.reflect.Method;
22+
23+
import org.springframework.asm.Type;
24+
import org.springframework.core.ResolvableType;
25+
import org.springframework.util.ClassUtils;
26+
import org.springframework.util.ReflectionUtils;
27+
28+
/**
29+
* Interface representing a member reference such as a field or method.
30+
*
31+
* @author Mark Paluch
32+
* @since 4.1
33+
*/
34+
sealed interface MemberDescriptor
35+
permits MemberDescriptor.MethodDescriptor.FieldDescriptor, MemberDescriptor.MethodDescriptor {
36+
37+
/**
38+
* @return class owning the member, can be the declaring class or a subclass.
39+
*/
40+
Class<?> getOwner();
41+
42+
/**
43+
* @return the member (field or method).
44+
*/
45+
Member getMember();
46+
47+
/**
48+
* @return field type or method return type.
49+
*/
50+
ResolvableType getType();
51+
52+
/**
53+
* Create {@link MethodDescriptor} from a serialized lambda representing a method reference.
54+
*/
55+
static MethodDescriptor ofMethodReference(ClassLoader classLoader, SerializedLambda lambda)
56+
throws ClassNotFoundException {
57+
return ofMethod(classLoader, Type.getObjectType(lambda.getImplClass()).getClassName(), lambda.getImplMethodName());
58+
}
59+
60+
/**
61+
* Create {@link MethodDescriptor} from owner type and method name.
62+
*/
63+
static MethodDescriptor ofMethod(ClassLoader classLoader, String ownerClassName, String name)
64+
throws ClassNotFoundException {
65+
Class<?> owner = ClassUtils.forName(ownerClassName, classLoader);
66+
return MethodDescriptor.create(owner, name);
67+
}
68+
69+
/**
70+
* Create {@link MethodDescriptor.FieldDescriptor} from owner type, field name and field type.
71+
*/
72+
public static MethodDescriptor.FieldDescriptor ofField(ClassLoader classLoader, String ownerClassName, String name,
73+
String fieldType) throws ClassNotFoundException {
74+
75+
Class<?> owner = ClassUtils.forName(ownerClassName, classLoader);
76+
Class<?> type = ClassUtils.forName(fieldType, classLoader);
77+
78+
return FieldDescriptor.create(owner, name, type);
79+
}
80+
81+
/**
82+
* Value object describing a {@link Method} in the context of an owning class.
83+
*
84+
* @param owner
85+
* @param method
86+
*/
87+
record MethodDescriptor(Class<?> owner, Method method) implements MemberDescriptor {
88+
89+
static MethodDescriptor create(Class<?> owner, String methodName) {
90+
Method method = ReflectionUtils.findMethod(owner, methodName);
91+
if (method == null) {
92+
throw new IllegalArgumentException("Method '%s.%s()' not found".formatted(owner.getName(), methodName));
93+
}
94+
return new MethodDescriptor(owner, method);
95+
}
96+
97+
@Override
98+
public Class<?> getOwner() {
99+
return owner();
100+
}
101+
102+
@Override
103+
public Method getMember() {
104+
return method();
105+
}
106+
107+
@Override
108+
public ResolvableType getType() {
109+
return ResolvableType.forMethodReturnType(method(), owner());
110+
}
111+
112+
}
113+
114+
/**
115+
* Value object describing a {@link Field} in the context of an owning class.
116+
*
117+
* @param owner
118+
* @param field
119+
*/
120+
record FieldDescriptor(Class<?> owner, Field field) implements MemberDescriptor {
121+
122+
static FieldDescriptor create(Class<?> owner, String fieldName, Class<?> fieldType) {
123+
124+
Field field = ReflectionUtils.findField(owner, fieldName, fieldType);
125+
if (field == null) {
126+
throw new IllegalArgumentException("Field '%s.%s' not found".formatted(owner.getName(), fieldName));
127+
}
128+
return new FieldDescriptor(owner, field);
129+
}
130+
131+
@Override
132+
public Class<?> getOwner() {
133+
return owner();
134+
}
135+
136+
@Override
137+
public Field getMember() {
138+
return field();
139+
}
140+
141+
@Override
142+
public ResolvableType getType() {
143+
return ResolvableType.forField(field(), owner());
144+
}
145+
146+
}
147+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public static Member resolve(Object object) {
3737
Assert.notNull(object, "Object must not be null");
3838
Assert.isInstanceOf(Serializable.class, object, "Object must be Serializable");
3939

40-
return new SamParser(MemberReference.class).parse(object.getClass().getClassLoader(), object).getMember();
40+
return new SerializableLambdaReader(MemberReference.class).read(object)
41+
.getMember();
4142
}
4243

4344
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,17 @@
3535
public interface PropertyPath extends Streamable<PropertyPath> {
3636

3737
/**
38-
* Syntax sugar to create a {@link TypedPropertyPath} from an existing one, ideal for method handles.
38+
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference or lambda.
39+
* <p>
40+
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference or lambda.
3941
*
40-
* @param propertyPath
41-
* @return
42+
* @param propertyPath the method reference or lambda.
4243
* @param <T> owning type.
43-
* @param <R> property type.
44-
* @since xxx
44+
* @param <P> property type.
45+
* @return the typed property path.
4546
*/
46-
public static <T, R> TypedPropertyPath<T, R> of(TypedPropertyPath<T, R> propertyPath) {
47-
return TypedPropertyPath.of(propertyPath);
47+
static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> propertyPath) {
48+
return TypedPropertyPaths.of(propertyPath);
4849
}
4950

5051
/**

0 commit comments

Comments
 (0)