Skip to content

Commit ec0c0d3

Browse files
mp911deodrotbohm
authored andcommitted
Introduce support to create domain objects via factory methods.
Issue #2476.
1 parent 81d179c commit ec0c0d3

35 files changed

+1320
-442
lines changed

src/main/asciidoc/object-mapping.adoc

+6-4
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ This means we need two fundamental steps:
1717
Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type.
1818
The resolution algorithm works as follows:
1919

20-
1. If there is a single constructor, it is used.
21-
2. If there are multiple constructors and exactly one is annotated with `@PersistenceConstructor`, it is used.
22-
3. If there's a no-argument constructor, it is used.
20+
1. If there is a single static factory method annotated with `@FactoryMethod` then it is used.
21+
2. If there is a single constructor, it is used.
22+
3. If there are multiple constructors and exactly one is annotated with `@PersistenceConstructor`, it is used.
23+
4. If there's a no-argument constructor, it is used.
2324
Other constructors will be ignored.
2425

25-
The value resolution assumes constructor argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
26+
The value resolution assumes constructor/factory method argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
2627
This also requires either parameter names information available in the class file or an `@ConstructorProperties` annotation being present on the constructor.
2728

2829
The value resolution can be customized by using Spring Framework's `@Value` value annotation using a store-specific SpEL expression.
@@ -206,6 +207,7 @@ Even if the intent is that the calculation should be preferred, it's important t
206207
<6> The class exposes a factory method and a constructor for object creation.
207208
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceConstructor`.
208209
Instead, defaulting of properties is handled within the factory method.
210+
If you want Spring Data to use the factory method for object instantiation, annotate it with `@FactoryMethod`.
209211

210212
[[mapping.general-recommendations]]
211213
== General recommendations
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2011-2021 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.annotation;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Marker annotation to declare a constructor or factory method annotation as factory/preferred constructor annotation.
25+
*
26+
* @author Mark Paluch
27+
* @since 3.0
28+
*/
29+
@Retention(RetentionPolicy.RUNTIME)
30+
@Target({ ElementType.ANNOTATION_TYPE })
31+
public @interface EntityCreatorAnnotation {
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2011-2021 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.annotation;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Annotation to declare a {@code static} method as factory method for class instantiation.
25+
*
26+
* @author Mark Paluch
27+
* @since 3.0
28+
*/
29+
@Retention(RetentionPolicy.RUNTIME)
30+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
31+
@EntityCreatorAnnotation
32+
public @interface FactoryMethod {
33+
}

src/main/java/org/springframework/data/annotation/PersistenceConstructor.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
import java.lang.annotation.Target;
2222

2323
/**
24+
* Annotation to declare a constructor for instantiation.
25+
*
2426
* @author Jon Brisbin
27+
* @author Mark Paluch
2528
*/
2629
@Retention(RetentionPolicy.RUNTIME)
27-
@Target(ElementType.CONSTRUCTOR)
30+
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
31+
@EntityCreatorAnnotation
2832
public @interface PersistenceConstructor {
2933
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2021 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.mapping;
17+
18+
import java.util.List;
19+
20+
/**
21+
* Metadata describing a mechanism to create an entity instance.
22+
*
23+
* @author Mark Paluch
24+
* @since 3.0
25+
*/
26+
public interface EntityCreatorMetadata<P extends PersistentProperty<P>> {
27+
28+
/**
29+
* Check whether the given {@link PersistentProperty} is being used as creator parameter.
30+
*
31+
* @param property
32+
* @return
33+
*/
34+
boolean isCreatorParameter(PersistentProperty<?> property);
35+
36+
/**
37+
* Returns whether the given {@link Parameter} is one referring to parent value (such as an enclosing class or a
38+
* receiver parameter).
39+
*
40+
* @param parameter
41+
* @return
42+
*/
43+
default boolean isParentParameter(Parameter<?, P> parameter) {
44+
return false;
45+
}
46+
47+
/**
48+
* @return the number of parameters.
49+
*/
50+
default int getParameterCount() {
51+
return getParameters().size();
52+
}
53+
54+
/**
55+
* @return the parameters used by this creator.
56+
*/
57+
List<Parameter<Object, P>> getParameters();
58+
59+
/**
60+
* @return whether the creator accepts {@link Parameter}s.
61+
*/
62+
default boolean hasParameters() {
63+
return !getParameters().isEmpty();
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2022 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.mapping;
17+
18+
import java.lang.reflect.Executable;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Value object to encapsulate the entity creation mechanism through a {@link Executable} to be used when mapping
28+
* persistent data to objects.
29+
*
30+
* @author Mark Paluch
31+
* @since 3.0
32+
*/
33+
class EntityCreatorMetadataSupport<T, P extends PersistentProperty<P>> implements EntityCreatorMetadata<P> {
34+
35+
private final Executable executable;
36+
private final List<Parameter<Object, P>> parameters;
37+
private final Map<PersistentProperty<?>, Boolean> isPropertyParameterCache = new ConcurrentHashMap<>();
38+
39+
/**
40+
* Creates a new {@link EntityCreatorMetadataSupport} from the given {@link Executable} and {@link Parameter}s.
41+
*
42+
* @param executable must not be {@literal null}.
43+
* @param parameters must not be {@literal null}.
44+
*/
45+
@SafeVarargs
46+
public EntityCreatorMetadataSupport(Executable executable, Parameter<Object, P>... parameters) {
47+
48+
Assert.notNull(executable, "Executable must not be null!");
49+
Assert.notNull(parameters, "Parameters must not be null!");
50+
51+
this.executable = executable;
52+
this.parameters = Arrays.asList(parameters);
53+
}
54+
55+
/**
56+
* Returns the underlying {@link Executable} that can be invoked reflectively.
57+
*
58+
* @return
59+
*/
60+
Executable getExecutable() {
61+
return executable;
62+
}
63+
64+
/**
65+
* Returns the {@link Parameter}s of the executable.
66+
*
67+
* @return
68+
*/
69+
public List<Parameter<Object, P>> getParameters() {
70+
return parameters;
71+
}
72+
73+
/**
74+
* Returns whether the given {@link PersistentProperty} is referenced in a creator argument of the
75+
* {@link PersistentEntity} backing this {@link EntityCreatorMetadataSupport}.
76+
* <p>
77+
* Results of this call are cached and reused on the next invocation. Calling this method for a
78+
* {@link PersistentProperty} that was not yet added to its owning {@link PersistentEntity} will capture that state
79+
* and return the same result after adding {@link PersistentProperty} to its entity.
80+
*
81+
* @param property must not be {@literal null}.
82+
* @return {@literal true} if the {@link PersistentProperty} is used in the creator.
83+
*/
84+
@Override
85+
public boolean isCreatorParameter(PersistentProperty<?> property) {
86+
87+
Assert.notNull(property, "Property must not be null!");
88+
89+
Boolean cached = isPropertyParameterCache.get(property);
90+
91+
if (cached != null) {
92+
return cached;
93+
}
94+
95+
boolean result = doGetIsCreatorParameter(property);
96+
97+
isPropertyParameterCache.put(property, result);
98+
99+
return result;
100+
}
101+
102+
@Override
103+
public String toString() {
104+
return executable.toString();
105+
}
106+
107+
private boolean doGetIsCreatorParameter(PersistentProperty<?> property) {
108+
109+
for (Parameter<?, P> parameter : parameters) {
110+
if (parameter.maps(property)) {
111+
return true;
112+
}
113+
}
114+
115+
return false;
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2011-2021 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.mapping;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.Method;
20+
21+
import org.springframework.util.ReflectionUtils;
22+
23+
/**
24+
* Value object to encapsulate the factory method to be used when mapping persistent data to objects.
25+
*
26+
* @author Mark Paluch
27+
* @since 3.0
28+
*/
29+
public final class FactoryMethod<T, P extends PersistentProperty<P>> extends EntityCreatorMetadataSupport<T, P> {
30+
31+
/**
32+
* Creates a new {@link FactoryMethod} from the given {@link Constructor} and {@link Parameter}s.
33+
*
34+
* @param factoryMethod must not be {@literal null}.
35+
* @param parameters must not be {@literal null}.
36+
*/
37+
@SafeVarargs
38+
public FactoryMethod(Method factoryMethod, Parameter<Object, P>... parameters) {
39+
40+
super(factoryMethod, parameters);
41+
ReflectionUtils.makeAccessible(factoryMethod);
42+
}
43+
44+
/**
45+
* Returns the underlying {@link Constructor}.
46+
*
47+
* @return
48+
*/
49+
public Method getFactoryMethod() {
50+
return (Method) getExecutable();
51+
}
52+
53+
}

0 commit comments

Comments
 (0)