Introduce support to create domain objects via factory methods.

Issue #2476.
This commit is contained in:
Mark Paluch
2021-10-06 15:13:36 +02:00
committed by Oliver Drotbohm
parent 0ee9c55f05
commit c4a324e3cf
35 changed files with 1274 additions and 424 deletions

View File

@@ -24,7 +24,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;

View File

@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PreferredConstructorDiscovererUnitTests.Outer.Inner;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
@@ -100,7 +99,7 @@ class PreferredConstructorDiscovererUnitTests<P extends PersistentProperty<P>> {
assertThat(PreferredConstructorDiscoverer.discover(entity)).satisfies(constructor -> {
Parameter<?, P> parameter = constructor.getParameters().iterator().next();
assertThat(constructor.isEnclosingClassParameter(parameter)).isTrue();
assertThat(constructor.isParentParameter(parameter)).isTrue();
});
}

View File

@@ -33,11 +33,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.annotation.FactoryMethod;
import org.springframework.data.classloadersupport.HidingClassLoader;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.ObjectInstantiator;
import org.springframework.data.mapping.model.ClassGeneratingEntityInstantiatorUnitTests.Outer.Inner;
import org.springframework.data.util.ClassTypeInformation;
@@ -56,7 +57,7 @@ import org.springframework.util.ReflectionUtils;
@MockitoSettings(strictness = Strictness.LENIENT)
class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>> {
ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator();
ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(false);
@Mock PersistentEntity<?, P> entity;
@Mock ParameterValueProvider<P> provider;
@@ -83,7 +84,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
PreferredConstructor<Foo, P> constructor = PreferredConstructorDiscoverer.discover(Foo.class);
doReturn(Foo.class).when(entity).getType();
doReturn(constructor).when(entity).getPersistenceConstructor();
doReturn(constructor).when(entity).getEntityCreator();
assertThat(instance.createInstance(entity, provider)).isInstanceOf(Foo.class);
@@ -105,7 +106,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
void createsInnerClassInstanceCorrectly() {
var entity = new BasicPersistentEntity<Inner, P>(from(Inner.class));
assertThat(entity.getPersistenceConstructor()).satisfies(constructor -> {
assertThat(entity.getEntityCreator()).satisfies(constructor -> {
var parameter = constructor.getParameters().iterator().next();
@@ -189,12 +190,43 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
assertThat(reference.sample.name).isEqualTo("FOO");
}
@Test // DATACMNS-1175
@SuppressWarnings({ "unchecked", "rawtypes" })
void createsInstancesWithFactoryMethodCorrectly() {
PersistentEntity<WithFactoryMethod, P> entity = new BasicPersistentEntity<>(from(WithFactoryMethod.class));
doReturn(2L, "FOO").when(provider).getParameterValue(any(Parameter.class));
var provider = new ParameterValueProvider<P>() {
@Override
public <T> T getParameterValue(Parameter<T, P> parameter) {
if (parameter.getName().equals("id")) {
return (T) Long.valueOf(1);
}
if (parameter.getName().equals("name")) {
return (T) "Walter";
}
throw new UnsupportedOperationException(parameter.getName());
}
};
var result = this.instance.createInstance(entity, provider);
assertThat(result.id).isEqualTo(1L);
assertThat(result.name).isEqualTo("Hello Walter");
}
@Test // DATACMNS-578, DATACMNS-1126
void instantiateObjCtorDefault() {
doReturn(ObjCtorDefault.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtorDefault.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
IntStream.range(0, 2)
.forEach(i -> assertThat(this.instance.createInstance(entity, provider)).isInstanceOf(ObjCtorDefault.class));
@@ -205,7 +237,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjCtorNoArgs.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtorNoArgs.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -224,7 +256,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjCtor1ParamString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtor1ParamString.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
doReturn("FOO").when(provider).getParameterValue(any());
IntStream.range(0, 2).forEach(i -> {
@@ -242,7 +274,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjCtor2ParamStringString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtor2ParamStringString.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -262,7 +294,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjectCtor1ParamInt.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor1ParamInt.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -280,7 +312,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjectCtor1ParamInt.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor1ParamInt.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
assertThatThrownBy(() -> this.instance.createInstance(entity, provider)) //
.hasCauseInstanceOf(IllegalArgumentException.class);
@@ -292,7 +324,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(ObjectCtor7ParamsString5IntsString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor7ParamsString5IntsString.class))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -412,7 +444,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
doReturn(type).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(type))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
}
static class Foo {
@@ -429,6 +461,23 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
}
}
static class WithFactoryMethod {
final Long id;
final String name;
private WithFactoryMethod(Long id, String name) {
this.id = id;
this.name = name;
}
@FactoryMethod
public static WithFactoryMethod create(Long id, String name) {
return new WithFactoryMethod(id, "Hello " + name);
}
}
static class Sample {
final Long id;

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mapping.model;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.FactoryMethod;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.util.ClassTypeInformation;
/**
* Unit tests for {@link EntityCreatorMetadataDiscoverer}.
*
* @author Mark Paluch
*/
class EntityCreatorMetadataDiscovererUnitTests {
@Test
void shouldDiscoverAnnotatedFactoryMethod() {
var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(FactoryMethodsPerson.class));
var creator = EntityCreatorMetadataDiscoverer.discover(entity);
assertThat(creator).isInstanceOf(org.springframework.data.mapping.FactoryMethod.class);
assertThat(((org.springframework.data.mapping.FactoryMethod<?, ?>) creator).getFactoryMethod().getParameterCount())
.isEqualTo(2);
}
@Test
void shouldDiscoverAnnotatedConstructor() {
var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(ConstructorPerson.class));
var creator = EntityCreatorMetadataDiscoverer.discover(entity);
assertThat(creator).isInstanceOf(PreferredConstructor.class);
}
@Test
void shouldDiscoverDefaultConstructor() {
var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(Person.class));
var creator = EntityCreatorMetadataDiscoverer.discover(entity);
assertThat(creator).isInstanceOf(PreferredConstructor.class);
}
@Test
void shouldRejectNonStaticFactoryMethod() {
assertThatExceptionOfType(MappingException.class)
.isThrownBy(() -> new BasicPersistentEntity<>(ClassTypeInformation.from(NonStaticFactoryMethod.class)));
}
static class Person {
private final String firstname, lastname;
private Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
static class NonStaticFactoryMethod {
@FactoryMethod
public ConstructorPerson of(String firstname, String lastname) {
return new ConstructorPerson(firstname, lastname);
}
}
static class FactoryMethodsPerson {
private final String firstname, lastname;
private FactoryMethodsPerson(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public static FactoryMethodsPerson of(String firstname) {
return new FactoryMethodsPerson(firstname, "unknown");
}
@FactoryMethod
public static FactoryMethodsPerson of(String firstname, String lastname) {
return new FactoryMethodsPerson(firstname, lastname);
}
}
static class ConstructorPerson {
private final String firstname, lastname;
private ConstructorPerson(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public static ConstructorPerson of(String firstname, String lastname) {
return new ConstructorPerson(firstname, lastname);
}
}
}

View File

@@ -44,13 +44,6 @@ class EntityInstantiatorsUnitTests {
assertThatIllegalArgumentException().isThrownBy(() -> new EntityInstantiators((EntityInstantiator) null));
}
@Test
void usesReflectionEntityInstantiatorAsDefaultFallback() {
var instantiators = new EntityInstantiators();
assertThat(instantiators.getInstantiatorFor(entity)).isInstanceOf(ClassGeneratingEntityInstantiator.class);
}
@Test
void returnsCustomInstantiatorForTypeIfRegistered() {

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mapping.model;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.FactoryMethod;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.util.ClassTypeInformation;
/**
* Unit tests for {@link org.springframework.data.mapping.FactoryMethod}.
*
* @author Mark Paluch
*/
class FactoryMethodUnitTests {
private static EntityInstantiators instantiators = new EntityInstantiators();
@Test
void shouldCreateInstanceThroughFactoryMethod() {
var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(FactoryPerson.class));
var result = instantiators.getInstantiatorFor(entity).createInstance(entity,
new ParameterValueProvider() {
@Override
public Object getParameterValue(Parameter parameter) {
if (parameter.getName().equals("firstname")) {
return "Walter";
}
if (parameter.getName().equals("lastname")) {
return "White";
}
return null;
}
});
assertThat(result.firstname).isEqualTo("Walter");
assertThat(result.lastname).isEqualTo("Mr. White");
}
static class FactoryPerson {
private final String firstname, lastname;
private FactoryPerson(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
@FactoryMethod
public static FactoryPerson of(String firstname, String lastname) {
return new FactoryPerson(firstname, "Mr. " + lastname);
}
}
}

View File

@@ -27,8 +27,8 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
import org.springframework.test.util.ReflectionTestUtils;

View File

@@ -55,7 +55,7 @@ class PersistentEntityParameterValueProviderUnitTests<P extends PersistentProper
}
};
assertThat(entity.getPersistenceConstructor()).satisfies(constructor -> {
assertThat(entity.getEntityCreator()).satisfies(constructor -> {
var iterator = constructor.getParameters().iterator();
ParameterValueProvider<P> provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider,
@@ -74,7 +74,7 @@ class PersistentEntityParameterValueProviderUnitTests<P extends PersistentProper
ParameterValueProvider<P> provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider,
Optional.of(property));
assertThat(entity.getPersistenceConstructor())
assertThat(entity.getEntityCreator())
.satisfies(constructor -> assertThatExceptionOfType(MappingException.class)//
.isThrownBy(() -> provider.getParameterValue(constructor.getParameters().iterator().next()))//
.withMessageContaining("bar")//

View File

@@ -30,10 +30,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.model.ReflectionEntityInstantiatorUnitTests.Outer.Inner;
import org.springframework.util.ReflectionUtils;
@@ -69,7 +69,7 @@ class ReflectionEntityInstantiatorUnitTests<P extends PersistentProperty<P>> {
PreferredConstructor<Foo, P> constructor = PreferredConstructorDiscoverer.discover(Foo.class);
doReturn(constructor).when(entity).getPersistenceConstructor();
doReturn(constructor).when(entity).getEntityCreator();
var instance = INSTANCE.createInstance(entity, provider);
@@ -92,7 +92,7 @@ class ReflectionEntityInstantiatorUnitTests<P extends PersistentProperty<P>> {
void createsInnerClassInstanceCorrectly() {
var entity = new BasicPersistentEntity<Inner, P>(from(Inner.class));
assertThat(entity.getPersistenceConstructor()).satisfies(it -> {
assertThat(entity.getEntityCreator()).satisfies(it -> {
var parameter = it.getParameters().iterator().next();

View File

@@ -27,7 +27,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.model.AbstractPersistentPropertyUnitTests.SamplePersistentProperty;
/**