Browse Source

DATACMNS-1206 - Polishing.

Moved PropertyDescriptor lookup into dedicated subclass to group functionality around the type and MethodsMetadata instances. Extracted individual stream handling steps into dedicated methods for better understandability.

Moved MethodsMetadataReader into classreading package for symmetry with Spring Framework's metadata arrangement. Removed manually declared getters in DefaultMethodsMetadataReader in favor of Lombok getters. Inlined MethodsMetadataReadingVisitor into DefaultMethodsMetadataReader as it's only used there.

Original pull request: #263.
pull/269/head
Oliver Gierke 8 years ago committed by Mark Paluch
parent
commit
5eb10a0817
  1. 223
      src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java
  2. 1
      src/main/java/org/springframework/data/type/MethodsMetadata.java
  3. 133
      src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java
  4. 5
      src/main/java/org/springframework/data/type/classreading/MethodsMetadataReader.java
  5. 1
      src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java
  6. 107
      src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java
  7. 1
      src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java
  8. 3
      src/test/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactoryUnitTests.java

223
src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java

@ -15,10 +15,11 @@ @@ -15,10 +15,11 @@
*/
package org.springframework.data.projection;
import lombok.extern.slf4j.Slf4j;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@ -29,11 +30,11 @@ import java.util.stream.IntStream; @@ -29,11 +30,11 @@ import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.springframework.beans.BeanUtils;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.data.type.MethodsMetadataReader;
import org.springframework.data.type.classreading.MethodsMetadataReader;
import org.springframework.data.type.classreading.MethodsMetadataReaderFactory;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -61,7 +62,7 @@ class DefaultProjectionInformation implements ProjectionInformation { @@ -61,7 +62,7 @@ class DefaultProjectionInformation implements ProjectionInformation {
Assert.notNull(type, "Projection type must not be null!");
this.projectionType = type;
this.properties = collectDescriptors(type);
this.properties = new PropertyDescriptorSource(type).getDescriptors();
}
/*
@ -107,103 +108,171 @@ class DefaultProjectionInformation implements ProjectionInformation { @@ -107,103 +108,171 @@ class DefaultProjectionInformation implements ProjectionInformation {
}
/**
* Collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces.
* Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method.
*
* @param type must not be {@literal null}.
* @param descriptor must not be {@literal null}.
* @return
*/
private static List<PropertyDescriptor> collectDescriptors(Class<?> type) {
private static boolean hasDefaultGetter(PropertyDescriptor descriptor) {
List<PropertyDescriptor> result = new ArrayList<>();
Method method = descriptor.getReadMethod();
Optional<MethodsMetadata> metadata = getMetadata(type);
Stream<PropertyDescriptor> stream = Arrays.stream(BeanUtils.getPropertyDescriptors(type))//
.filter(it -> !hasDefaultGetter(it));
return method == null ? false : method.isDefault();
}
Stream<PropertyDescriptor> streamToUse = metadata.map(DefaultProjectionInformation::getMethodOrder)
.filter(it -> !it.isEmpty()) //
.map(it -> stream.filter(descriptor -> it.containsKey(descriptor.getReadMethod().getName()))
.sorted(Comparator.comparingInt(left -> it.get(left.getReadMethod().getName())))) //
.orElse(stream);
/**
* Internal helper to detect {@link PropertyDescriptor} instances for a given type.
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.1
* @soundtrack The Meters - Cissy Strut (Here Comes The Meter Man)
*/
@Slf4j
private static class PropertyDescriptorSource {
result.addAll(streamToUse.collect(Collectors.toList()));
private final Class<?> type;
private final Optional<MethodsMetadata> metadata;
if (metadata.isPresent()) {
/**
* Creates a new {@link PropertyDescriptorSource} for the given type.
*
* @param type must not be {@literal null}.
*/
public PropertyDescriptorSource(Class<?> type) {
Stream<String> interfaceNames = metadata.map(ClassMetadata::getInterfaceNames) //
.map(Arrays::stream) //
.orElse(Stream.empty());
Assert.notNull(type, "Type must not be null!");
result.addAll(interfaceNames.map(it -> loadClass(it, type.getClassLoader())) //
.map(DefaultProjectionInformation::collectDescriptors) //
.flatMap(List::stream) //
.collect(Collectors.toList()));
} else {
this.type = type;
this.metadata = getMetadata(type);
}
for (Class<?> interfaze : type.getInterfaces()) {
result.addAll(collectDescriptors(interfaze));
}
/**
* Returns {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces.
*
* @return
*/
public List<PropertyDescriptor> getDescriptors() {
return collectDescriptors().distinct().collect(StreamUtils.toUnmodifiableList());
}
return result.stream().distinct().collect(Collectors.toList());
}
/**
* Recursively collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super
* interfaces.
*
* @return
*/
private Stream<PropertyDescriptor> collectDescriptors() {
Stream<PropertyDescriptor> allButDefaultGetters = Arrays.stream(BeanUtils.getPropertyDescriptors(type)) //
.filter(it -> !hasDefaultGetter(it));
Stream<PropertyDescriptor> ownDescriptors = metadata.map(it -> filterAndOrder(allButDefaultGetters, it))
.orElse(allButDefaultGetters);
private static Class<?> loadClass(String className, ClassLoader classLoader) {
Stream<PropertyDescriptor> superTypeDescriptors = metadata.map(this::fromMetadata) //
.orElseGet(this::fromType) //
.flatMap(it -> new PropertyDescriptorSource(it).collectDescriptors());
try {
return ClassUtils.forName(className, classLoader);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(String.format("Cannot load class %s", className));
return Stream.concat(ownDescriptors, superTypeDescriptors);
}
}
/**
* Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}.
*
* @param metadata
* @return
*/
private static Map<String, Integer> getMethodOrder(MethodsMetadata metadata) {
/**
* Returns a Stream of {@link PropertyDescriptor} ordered following the given {@link MethodsMetadata} only returning
* methods seen by the given {@link MethodsMetadata}.
*
* @param source must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @return
*/
private static Stream<PropertyDescriptor> filterAndOrder(Stream<PropertyDescriptor> source,
MethodsMetadata metadata) {
Map<String, Integer> orderedMethods = getMethodOrder(metadata);
if (orderedMethods.isEmpty()) {
return source;
}
List<String> methods = metadata.getMethods() //
.stream() //
.map(MethodMetadata::getMethodName) //
.distinct() //
.collect(Collectors.toList());
return source.filter(descriptor -> orderedMethods.containsKey(descriptor.getReadMethod().getName()))
.sorted(Comparator.comparingInt(left -> orderedMethods.get(left.getReadMethod().getName())));
}
return IntStream.range(0, methods.size()) //
.boxed() //
.collect(Collectors.toMap(methods::get, i -> i));
}
/**
* Returns a {@link Stream} of interfaces using the given {@link MethodsMetadata} as primary source for ordering.
*
* @param metadata must not be {@literal null}.
* @return
*/
private Stream<Class<?>> fromMetadata(MethodsMetadata metadata) {
return Arrays.stream(metadata.getInterfaceNames()).map(it -> findType(it, type.getInterfaces()));
}
/**
* Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing
* {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise.
*
* @param type must not be {@literal null}.
* @return the optional {@link MethodsMetadata}.
*/
private static Optional<MethodsMetadata> getMetadata(Class<?> type) {
/**
* Returns a Stream of interfaces using the given type as primary source for ordering.
*
* @return
*/
private Stream<Class<?>> fromType() {
return Arrays.stream(type.getInterfaces());
}
try {
/**
* Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing
* {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise.
*
* @param type must not be {@literal null}.
* @return the optional {@link MethodsMetadata}.
*/
private static Optional<MethodsMetadata> getMetadata(Class<?> type) {
MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader());
MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type));
return Optional.of(metadataReader.getMethodsMetadata());
} catch (IOException e) {
return Optional.empty();
try {
MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader());
MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type));
return Optional.of(metadataReader.getMethodsMetadata());
} catch (IOException e) {
LOG.info("Couldn't read class metadata for {}. Input property calculation might fail!", type);
return Optional.empty();
}
}
}
/**
* Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method.
*
* @param descriptor must not be {@literal null}.
* @return
*/
private static boolean hasDefaultGetter(PropertyDescriptor descriptor) {
/**
* Find the type with the given name in the given array of {@link Class}.
*
* @param name must not be {@literal null} or empty.
* @param types must not be {@literal null}.
* @return
*/
private static Class<?> findType(String name, Class<?>[] types) {
return Arrays.stream(types) //
.filter(it -> name.equals(it.getName())) //
.findFirst()
.orElseThrow(() -> new IllegalStateException(String.format("Did not find type %s in %s!", name, types)));
}
Method method = descriptor.getReadMethod();
return method == null ? false : method.isDefault();
/**
* Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}.
*
* @param metadata
* @return
*/
private static Map<String, Integer> getMethodOrder(MethodsMetadata metadata) {
List<String> methods = metadata.getMethods() //
.stream() //
.map(MethodMetadata::getMethodName) //
.distinct() //
.collect(Collectors.toList());
return IntStream.range(0, methods.size()) //
.boxed() //
.collect(Collectors.toMap(methods::get, i -> i));
}
}
}

1
src/main/java/org/springframework/data/type/MethodsMetadata.java

@ -19,6 +19,7 @@ import java.util.Set; @@ -19,6 +19,7 @@ import java.util.Set;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.data.type.classreading.MethodsMetadataReader;
/**
* Interface that defines abstract metadata of a specific class, in a form that does not require that class to be loaded

133
src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java

@ -15,18 +15,29 @@ @@ -15,18 +15,29 @@
*/
package org.springframework.data.type.classreading;
import lombok.Getter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Set;
import org.springframework.asm.ClassReader;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
import org.springframework.asm.Type;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor;
import org.springframework.core.type.classreading.MethodMetadataReadingVisitor;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.data.type.MethodsMetadataReader;
import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link MethodsMetadataReader} implementation based on an ASM {@link org.springframework.asm.ClassReader}.
@ -34,6 +45,7 @@ import org.springframework.lang.Nullable; @@ -34,6 +45,7 @@ import org.springframework.lang.Nullable;
* @author Mark Paluch
* @since 2.1
*/
@Getter
class DefaultMethodsMetadataReader implements MethodsMetadataReader {
private final Resource resource;
@ -43,57 +55,96 @@ class DefaultMethodsMetadataReader implements MethodsMetadataReader { @@ -43,57 +55,96 @@ class DefaultMethodsMetadataReader implements MethodsMetadataReader {
DefaultMethodsMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
MethodsMetadataReadingVisitor visitor = new MethodsMetadataReadingVisitor(classLoader);
createClassReader(resource).accept(visitor, ClassReader.SKIP_DEBUG);
this.resource = resource;
this.classMetadata = visitor;
this.annotationMetadata = visitor;
this.methodsMetadata = visitor;
}
ClassReader classReader;
private static ClassReader createClassReader(Resource resource) throws IOException {
try (InputStream is = new BufferedInputStream(resource.getInputStream())) {
return new ClassReader(is);
try (InputStream is = new BufferedInputStream(getResource().getInputStream())) {
classReader = new ClassReader(is);
} catch (IllegalArgumentException ex) {
throw new NestedIOException("ASM ClassReader failed to parse class file - "
+ "probably due to a new Java class file version that isn't supported yet: " + getResource(), ex);
+ "probably due to a new Java class file version that isn't supported yet: " + resource, ex);
}
MethodsMetadataReadingVisitor visitor = new MethodsMetadataReadingVisitor(classLoader);
classReader.accept(visitor, ClassReader.SKIP_DEBUG);
classMetadata = visitor;
annotationMetadata = visitor;
methodsMetadata = visitor;
}
/*
* (non-Javadoc)
* @see org.springframework.core.type.classreading.MetadataReader#getResource()
/**
* ASM class visitor which looks for the class name and implemented types as well as for the methods defined in the
* class, exposing them through the {@link MethodsMetadata} interface.
*
* @author Mark Paluch
* @since 2.1
* @see ClassMetadata
* @see MethodMetadata
* @see MethodMetadataReadingVisitor
*/
@Override
public Resource getResource() {
return resource;
}
private static class MethodsMetadataReadingVisitor extends AnnotationMetadataReadingVisitor
implements MethodsMetadata {
/*
* (non-Javadoc)
* @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata()
*/
@Override
public ClassMetadata getClassMetadata() {
return classMetadata;
}
/**
* Construct a new {@link MethodsMetadataReadingVisitor} given {@link ClassLoader}.
*
* @param classLoader may be {@literal null}.
*/
MethodsMetadataReadingVisitor(@Nullable ClassLoader classLoader) {
super(classLoader);
}
/*
* (non-Javadoc)
* @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata()
*/
@Override
public AnnotationMetadata getAnnotationMetadata() {
return annotationMetadata;
}
/*
* (non-Javadoc)
* @see org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
*/
@Override
@SuppressWarnings("null")
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
/* (non-Javadoc)
* @see org.springframework.data.util.ClassMetadataReader#getMethodsMetadata()
*/
@Override
public MethodsMetadata getMethodsMetadata() {
return methodsMetadata;
// Skip bridge methods - we're only interested in original user methods.
// On JDK 8, we'd otherwise run into double detection of the same method...
if ((access & Opcodes.ACC_BRIDGE) != 0) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
// Skip constructors
if (name.equals("<init>")) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
MethodMetadataReadingVisitor visitor = new MethodMetadataReadingVisitor(name, access, getClassName(),
Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet);
this.methodMetadataSet.add(visitor);
return visitor;
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.MethodsMetadata#getMethods()
*/
@Override
public Set<MethodMetadata> getMethods() {
return Collections.unmodifiableSet(methodMetadataSet);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.MethodsMetadata#getMethods(String)
*/
@Override
public Set<MethodMetadata> getMethods(String name) {
Assert.hasText(name, "Method name must not be null or empty");
return methodMetadataSet.stream() //
.filter(it -> it.getMethodName().equals(name)) //
.collect(StreamUtils.toUnmodifiableSet());
}
}
}

5
src/main/java/org/springframework/data/type/MethodsMetadataReader.java → src/main/java/org/springframework/data/type/classreading/MethodsMetadataReader.java

@ -13,9 +13,10 @@ @@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.type;
package org.springframework.data.type.classreading;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.data.type.MethodsMetadata;
/**
* Extension to {@link MetadataReader} for accessing class metadata and method metadata as read by an ASM
@ -27,7 +28,7 @@ import org.springframework.core.type.classreading.MetadataReader; @@ -27,7 +28,7 @@ import org.springframework.core.type.classreading.MetadataReader;
public interface MethodsMetadataReader extends MetadataReader {
/**
* @return the metadata for methods in the class file.
* @return the {@link MethodsMetadata} for methods in the class file.
*/
MethodsMetadata getMethodsMetadata();
}

1
src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java

@ -21,7 +21,6 @@ import org.springframework.core.io.Resource; @@ -21,7 +21,6 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.data.type.MethodsMetadataReader;
import org.springframework.lang.Nullable;
/**

107
src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java

@ -1,107 +0,0 @@ @@ -1,107 +0,0 @@
/*
* Copyright 2018 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
*
* http://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.type.classreading;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
import org.springframework.asm.Type;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor;
import org.springframework.core.type.classreading.MethodMetadataReadingVisitor;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* ASM class visitor which looks for the class name and implemented types as well as for the methods defined in the
* class, exposing them through the {@link MethodsMetadata} interface.
*
* @author Mark Paluch
* @since 2.1
* @see ClassMetadata
* @see MethodMetadata
* @see MethodMetadataReadingVisitor
*/
class MethodsMetadataReadingVisitor extends AnnotationMetadataReadingVisitor implements MethodsMetadata {
/**
* Construct a new {@link MethodsMetadataReadingVisitor} given {@link ClassLoader}.
*
* @param classLoader may be {@literal null}.
*/
MethodsMetadataReadingVisitor(@Nullable ClassLoader classLoader) {
super(classLoader);
}
/*
* (non-Javadoc)
* @see org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
*/
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// Skip bridge methods - we're only interested in original user methods.
// On JDK 8, we'd otherwise run into double detection of the same method...
if ((access & Opcodes.ACC_BRIDGE) != 0) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
// Skip constructors
if (name.equals("<init>")) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
MethodMetadataReadingVisitor visitor = new MethodMetadataReadingVisitor(name, access, getClassName(),
Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet);
this.methodMetadataSet.add(visitor);
return visitor;
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.MethodsMetadata#getMethods()
*/
@Override
public Set<MethodMetadata> getMethods() {
return Collections.unmodifiableSet(methodMetadataSet);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.MethodsMetadata#getMethods(String)
*/
@Override
public Set<MethodMetadata> getMethods(String name) {
Assert.hasText(name, "Method name must not be null or empty");
Set<MethodMetadata> result = new LinkedHashSet<>(4);
for (MethodMetadata metadata : methodMetadataSet) {
if (metadata.getMethodName().equals(name)) {
result.add(metadata);
}
}
return Collections.unmodifiableSet(result);
}
}

1
src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java

@ -23,7 +23,6 @@ import java.util.Iterator; @@ -23,7 +23,6 @@ import java.util.Iterator;
import org.junit.Test;
import org.springframework.core.type.MethodMetadata;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.data.type.MethodsMetadataReader;
/**
* Unit tests for {@link DefaultMethodsMetadataReader}.

3
src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java → src/test/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactoryUnitTests.java

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.type;
package org.springframework.data.type.classreading;
import static org.assertj.core.api.Assertions.*;
@ -24,6 +24,7 @@ import java.net.URLClassLoader; @@ -24,6 +24,7 @@ import java.net.URLClassLoader;
import org.junit.Test;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.data.type.classreading.MethodsMetadataReader;
import org.springframework.data.type.classreading.MethodsMetadataReaderFactory;
/**
Loading…
Cancel
Save