hick hack - annotation support for properties

This commit is contained in:
Christoph Strobl
2020-10-12 14:13:33 +02:00
parent 6d5d9776c9
commit b61c1abd7b
8 changed files with 573 additions and 31 deletions

View File

@@ -16,12 +16,14 @@
package org.springframework.data.mapping.context;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
@@ -380,9 +382,10 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
// ((StaticTypeInformation<?>)typeInformation).doWithProperties()
Map<String, TypeInformation<?>> properties = ((StaticTypeInformation<?>) typeInformation).getProperties();
Map<String, List<Annotation>> annotations = ((StaticTypeInformation<?>) typeInformation).getPropertyAnnotations();
for (Entry<String, TypeInformation<?>> entry : properties.entrySet()) {
P target = createPersistentProperty(Property.of(typeInformation, entry.getKey()), entity, simpleTypeHolder);
P target = createPersistentProperty(Property.of(entry.getValue(), entry.getKey(), annotations.get(entry.getKey())), entity, simpleTypeHolder);
entity.addPersistentProperty(target);
}

View File

@@ -0,0 +1,331 @@
/*
* Copyright 2011-2020 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 java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.annotation.AccessType;
import org.springframework.data.annotation.AccessType.Type;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.Version;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Special {@link PersistentProperty} that takes annotations at a property into account.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
*/
public abstract class AnnotationBasedPersistentProperty<P extends PersistentProperty<P>>
extends AbstractPersistentProperty<P> {
private static final String SPRING_DATA_PACKAGE = "org.springframework.data";
private final @Nullable String value;
private final Map<Class<? extends Annotation>, Optional<? extends Annotation>> annotationCache = new ConcurrentHashMap<>();
private final Lazy<Boolean> usePropertyAccess = Lazy.of(() -> {
AccessType accessType = findPropertyOrOwnerAnnotation(AccessType.class);
return accessType != null && Type.PROPERTY.equals(accessType.value()) || super.usePropertyAccess();
});
private final Lazy<Boolean> isTransient = Lazy.of(() -> super.isTransient() || isAnnotationPresent(Transient.class)
|| isAnnotationPresent(Value.class) || isAnnotationPresent(Autowired.class));
private final Lazy<Boolean> isWritable = Lazy
.of(() -> !isTransient() && !isAnnotationPresent(ReadOnlyProperty.class));
private final Lazy<Boolean> isReference = Lazy.of(() -> !isTransient() && isAnnotationPresent(Reference.class));
private final Lazy<Boolean> isId = Lazy.of(() -> isAnnotationPresent(Id.class));
private final Lazy<Boolean> isVersion = Lazy.of(() -> isAnnotationPresent(Version.class));
/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
*
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
*/
public AnnotationBasedPersistentProperty(Property property, PersistentEntity<?, P> owner,
SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
populateAnnotationCache(property);
Value value = findAnnotation(Value.class);
this.value = value == null ? null : value.value();
}
/**
* Populates the annotation cache by eagerly accessing the annotations directly annotated to the accessors (if
* available) and the backing field. Annotations override annotations found on field.
*
* @param property
* @throws MappingException in case we find an ambiguous mapping on the accessor methods
*/
private void populateAnnotationCache(Property property) {
property.getAnnotations().forEach(it -> {
System.out.println("registering static annotation "+it.annotationType()+" for field " + property.getName());
annotationCache.put(it.annotationType(), Optional.of(it));
});
Optionals.toStream(property.getGetter(), property.getSetter()).forEach(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class<? extends Annotation> annotationType = annotation.annotationType();
validateAnnotation(annotation,
"Ambiguous mapping! Annotation %s configured "
+ "multiple times on accessor methods of property %s in class %s!",
annotationType.getSimpleName(), getName(), getOwner().getType().getSimpleName());
annotationCache.put(annotationType,
Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(it, annotationType)));
}
});
property.getField().ifPresent(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class<? extends Annotation> annotationType = annotation.annotationType();
validateAnnotation(annotation,
"Ambiguous mapping! Annotation %s configured " + "on field %s and one of its accessor methods in class %s!",
annotationType.getSimpleName(), it.getName(), getOwner().getType().getSimpleName());
annotationCache.put(annotationType,
Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(it, annotationType)));
}
});
}
/**
* Verifies the given annotation candidate detected. Will be rejected if it's a Spring Data annotation and we already
* found another one with a different configuration setup (i.e. other attribute values).
*
* @param candidate must not be {@literal null}.
* @param message must not be {@literal null}.
* @param arguments must not be {@literal null}.
*/
private void validateAnnotation(Annotation candidate, String message, Object... arguments) {
Class<? extends Annotation> annotationType = candidate.annotationType();
if (!annotationType.getName().startsWith(SPRING_DATA_PACKAGE)) {
return;
}
if (annotationCache.containsKey(annotationType)
&& !annotationCache.get(annotationType).equals(Optional.of(candidate))) {
throw new MappingException(String.format(message, arguments));
}
}
/**
* Inspects a potentially available {@link Value} annotation at the property and returns the {@link String} value of
* it.
*
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#getSpelExpression()
*/
@Nullable
@Override
public String getSpelExpression() {
return value;
}
/**
* Considers plain transient fields, fields annotated with {@link Transient}, {@link Value} or {@link Autowired} as
* transient.
*
* @see org.springframework.data.mapping.PersistentProperty#isTransient()
*/
@Override
public boolean isTransient() {
return isTransient.get();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#isIdProperty()
*/
public boolean isIdProperty() {
return isId.get();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#isVersionProperty()
*/
public boolean isVersionProperty() {
return isVersion.get();
}
/**
* Considers the property an {@link Association} if it is annotated with {@link Reference}.
*/
@Override
public boolean isAssociation() {
return isReference.get();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#isWritable()
*/
@Override
public boolean isWritable() {
return isWritable.get();
}
/**
* Returns the annotation found for the current {@link AnnotationBasedPersistentProperty}. Will prefer getters or
* setters annotations over ones found at the backing field as the former can be used to reconfigure the metadata in
* subclasses.
*
* @param annotationType must not be {@literal null}.
* @return {@literal null} if annotation type not found on property.
*/
@Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null!");
return doFindAnnotation(annotationType).orElse(null);
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
Optional<? extends Annotation> annotation = annotationCache.get(annotationType);
if (annotation != null) {
return (Optional<A>) annotation;
}
return (Optional<A>) annotationCache.computeIfAbsent(annotationType, type -> {
return getAccessors() //
.map(it -> AnnotatedElementUtils.findMergedAnnotation(it, type)) //
.flatMap(StreamUtils::fromNullable) //
.findFirst();
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#findPropertyOrOwnerAnnotation(java.lang.Class)
*/
@Nullable
@Override
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
A annotation = findAnnotation(annotationType);
return annotation != null ? annotation : getOwner().findAnnotation(annotationType);
}
/**
* Returns whether the property carries the an annotation of the given type.
*
* @param annotationType the annotation type to look up.
* @return
*/
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return doFindAnnotation(annotationType).isPresent();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#usePropertyAccess()
*/
@Override
public boolean usePropertyAccess() {
return usePropertyAccess.get();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#getAssociationTargetType()
*/
@Nullable
@Override
public Class<?> getAssociationTargetType() {
Reference reference = findAnnotation(Reference.class);
if (reference == null) {
return isEntity() ? getActualType() : null;
}
Class<?> targetType = reference.to();
return Class.class.equals(targetType) //
? isEntity() ? getActualType() : null //
: targetType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString()
*/
@Override
public String toString() {
if (annotationCache.isEmpty()) {
populateAnnotationCache(getProperty());
}
String builder = annotationCache.values().stream() //
.flatMap(Optionals::toStream) //
.map(Object::toString) //
.collect(Collectors.joining(" "));
return builder + super.toString();
}
private Stream<? extends AnnotatedElement> getAccessors() {
return Optionals.toStream(Optional.ofNullable(getGetter()), Optional.ofNullable(getSetter()),
Optional.ofNullable(getField()));
}
}

View File

@@ -17,8 +17,11 @@ package org.springframework.data.mapping.model;
import java.beans.FeatureDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -41,6 +44,8 @@ import org.springframework.util.StringUtils;
public class Property {
private @Nullable TypeInformation<?> typeInformation;
private List<Annotation> annotations;
private final Optional<Field> field;
private final Optional<PropertyDescriptor> descriptor;
@@ -53,8 +58,9 @@ public class Property {
private final Lazy<String> toString;
private final Lazy<Optional<Method>> wither;
private Property(String name, TypeInformation<?> typeInformation) {
private Property(String name, TypeInformation<?> typeInformation, List<Annotation> annotations) {
this.annotations = annotations;
this.typeInformation = typeInformation;
this.field = Optional.empty();
this.descriptor = Optional.empty();
@@ -73,6 +79,7 @@ public class Property {
Assert.notNull(type, "Type must not be null!");
Assert.isTrue(Optionals.isAnyPresent(field, descriptor), "Either field or descriptor has to be given!");
this.annotations = Collections.emptyList();
this.field = field;
this.descriptor = descriptor;
@@ -135,7 +142,11 @@ public class Property {
* @return
*/
public static Property of(TypeInformation<?> type, String name) {
return new Property(name, type);
return new Property(name, type, Collections.emptyList());
}
public static Property of(TypeInformation<?> type, String name, List<Annotation> annotations) {
return new Property(name, type, annotations != null ? annotations : Collections.emptyList());
}
/**
@@ -240,6 +251,10 @@ public class Property {
return rawType;
}
public List<Annotation> getAnnotations() {
return annotations;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)

View File

@@ -31,6 +31,8 @@
*/
package org.springframework.data.util;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @since 2020/10
@@ -53,19 +55,38 @@ public class Address {
return street;
}
// public void setCity(String city) {
// this.city = city;
// }
//
// public void setStreet(String street) {
// this.street = street;
// }
// public void setCity(String city) {
// this.city = city;
// }
//
// public void setStreet(String street) {
// this.street = street;
// }
@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
", street='" + street + '\'' +
'}';
return "Address{" + "city='" + city + '\'' + ", street='" + street + '\'' + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Address address = (Address) o;
if (!ObjectUtils.nullSafeEquals(city, address.city)) {
return false;
}
return ObjectUtils.nullSafeEquals(street, address.street);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(city);
result = 31 * result + ObjectUtils.nullSafeHashCode(street);
return result;
}
}

View File

@@ -33,6 +33,8 @@ package org.springframework.data.util;
import java.util.List;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @since 2020/10
@@ -100,13 +102,43 @@ public class Person {
@Override
public String toString() {
return "Person{" +
"id=" + id +
", firstname='" + firstname + '\'' +
", lastname='" + lastname + '\'' +
", age=" + age +
", address=" + address +
", nicknames=" + nicknames +
'}';
return "Person{" + "id=" + id + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + ", age="
+ age + ", address=" + address + ", nicknames=" + nicknames + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Person person = (Person) o;
if (id != person.id)
return false;
if (age != person.age)
return false;
if (!ObjectUtils.nullSafeEquals(firstname, person.firstname)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(lastname, person.lastname)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(address, person.address)) {
return false;
}
return ObjectUtils.nullSafeEquals(nicknames, person.nicknames);
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + ObjectUtils.nullSafeHashCode(firstname);
result = 31 * result + ObjectUtils.nullSafeHashCode(lastname);
result = 31 * result + age;
result = 31 * result + ObjectUtils.nullSafeHashCode(address);
result = 31 * result + ObjectUtils.nullSafeHashCode(nicknames);
return result;
}
}

View File

@@ -32,6 +32,7 @@
package org.springframework.data.util;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -44,6 +45,8 @@ import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
/**
* @author Christoph Strobl
@@ -133,4 +136,39 @@ public class PersonTypeInformation extends StaticTypeInformation<Person> {
protected PreferredConstructor computePreferredConstructor() {
return StaticPreferredConstructor.of("firstname", "lastname");
}
@Override
protected Map<String, List<Annotation>> computePropertyAnnotations() {
Map<String, List<Annotation>> annotationMap = new LinkedHashMap<>();
annotationMap.put("firstname", Collections.singletonList(new Field() {
@Override
public Class<? extends Annotation> annotationType() {
return Field.class;
}
@Override
public String value() {
return "first-name";
}
@Override
public String name() {
return value();
}
@Override
public int order() {
return 0;
}
@Override
public FieldType targetType() {
return FieldType.IMPLICIT;
}
}));
return annotationMap;
}
}

View File

@@ -31,6 +31,7 @@
*/
package org.springframework.data.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -62,6 +63,7 @@ public class StaticTypeInformation<S> extends ClassTypeInformation<S> {
private final Map<String, TypeInformation<?>> properties;
private final Map<String, BiFunction<S,Object,S>> setter;
private final Map<String, Function<S,Object>> getter;
private final Map<String, List<Annotation>> propertyAnnotations;
private EntityInstantiator instantiator;
private PreferredConstructor preferredConstructor;
@@ -83,6 +85,7 @@ public class StaticTypeInformation<S> extends ClassTypeInformation<S> {
this.setter = computeSetter();
this.getter = computeGetter();
this.preferredConstructor = computePreferredConstructor();
this.propertyAnnotations = computePropertyAnnotations();
}
protected Map<String, TypeInformation<?>> computePropertiesMap() {
@@ -113,6 +116,10 @@ public class StaticTypeInformation<S> extends ClassTypeInformation<S> {
return Collections.emptyMap();
}
protected Map<String, List<Annotation>> computePropertyAnnotations() {
return Collections.emptyMap();
}
public Map<String, TypeInformation<?>> getProperties() {
return properties;
}
@@ -129,6 +136,10 @@ public class StaticTypeInformation<S> extends ClassTypeInformation<S> {
return instantiator;
}
public Map<String, List<Annotation>> getPropertyAnnotations() {
return propertyAnnotations;
}
@Override
public List<TypeInformation<?>> getParameterTypes(Constructor<?> constructor) {
return null;

View File

@@ -23,6 +23,7 @@ import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
@@ -78,12 +79,11 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.PersonPojoStringId;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.util.Address;
import org.springframework.data.util.AddressTypeInformation;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Person;
import org.springframework.data.util.PersonTypeInformation;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.StopWatch;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
@@ -2182,14 +2182,75 @@ public class MappingMongoConverterUnitTests {
assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L);
}
@Test
public void perf1() {
ClassTypeInformation.warmCache(new PersonTypeInformation(), new AddressTypeInformation());
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setInitialEntitySet(new LinkedHashSet<>(
Arrays.asList(org.springframework.data.util.Person.class, org.springframework.data.util.Address.class)));
mappingContext.initialize();
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
org.springframework.data.util.Person source = new org.springframework.data.util.Person("spring", "data");
source.setAddress(new org.springframework.data.util.Address("the city", "never sleeps"));
source.setAge(10);
source.setId(9876);
source.setNicknames(Arrays.asList("tick", "trick", "track"));
StopWatch stopWatch = new StopWatch();
List<org.bson.Document> sources = new ArrayList<>();
stopWatch.start("write");
for (int i = 0; i < 10000; i++) {
org.bson.Document targetDocument = new org.bson.Document();
converter.write(source, targetDocument);
sources.add(targetDocument);
}
stopWatch.stop();
stopWatch.start("read");
for (org.bson.Document sourceDoc : sources) {
assertThat(converter.read(org.springframework.data.util.Person.class, sourceDoc)).isEqualTo(source);
}
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
// public void perf2() {
//
// ClassTypeInformation.warmCache(new PersonTypeInformation(), new AddressTypeInformation());
//
// MongoMappingContext mappingContext = new MongoMappingContext();
// mappingContext.setInitialEntitySet(new LinkedHashSet<>(Arrays.asList(org.springframework.data.util.Person.class,
// org.springframework.data.util.Address.class)));
// mappingContext.initialize();
//
// MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
//
// org.springframework.data.util.Person source = new org.springframework.data.util.Person("spring", "data");
// source.setAddress(new org.springframework.data.util.Address("the city", "never sleeps"));
// source.setAge(10);
// source.setId(9876);
// source.setNicknames(Arrays.asList("tick", "trick", "track"));
//
// }
@Test
public void xxx() {
ClassTypeInformation.warmCache(new PersonTypeInformation(), new AddressTypeInformation());
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setInitialEntitySet(new LinkedHashSet<>(Arrays.asList(org.springframework.data.util.Person.class, org.springframework.data.util.Address.class)));
mappingContext.setInitialEntitySet(new LinkedHashSet<>(
Arrays.asList(org.springframework.data.util.Person.class, org.springframework.data.util.Address.class)));
mappingContext.initialize();
org.springframework.data.util.Person source = new org.springframework.data.util.Person("spring", "data");
@@ -2197,17 +2258,17 @@ public class MappingMongoConverterUnitTests {
source.setAge(10);
source.setId(9876);
source.setNicknames(Arrays.asList("tick", "trick", "track"));
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
org.bson.Document targetDocument = new org.bson.Document();
converter.write(source, targetDocument);
System.out.println("target: " + targetDocument);
org.springframework.data.util.Person targetEntity = converter.read(org.springframework.data.util.Person.class, targetDocument);
org.springframework.data.util.Person targetEntity = converter.read(org.springframework.data.util.Person.class,
targetDocument);
System.out.println("targetEntity: " + targetEntity);
}
static class GenericType<T> {
@@ -2680,4 +2741,34 @@ public class MappingMongoConverterUnitTests {
return entity;
}
}
void xxx2() {
new Field() {
@Override
public Class<? extends Annotation> annotationType() {
return null;
}
@Override
public String value() {
return null;
}
@Override
public String name() {
return null;
}
@Override
public int order() {
return 0;
}
@Override
public FieldType targetType() {
return null;
}
};
}
}