diff --git a/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java index 228753dd2..738c14840 100644 --- a/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java @@ -23,10 +23,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; - import org.springframework.aop.ProxyMethodInvocation; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; @@ -56,15 +56,10 @@ public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor */ public static boolean hasDefaultMethods(Class interfaceClass) { - Method[] methods = ReflectionUtils.getAllDeclaredMethods(interfaceClass); - - for (Method method : methods) { - if (method.isDefault()) { - return true; - } - } + AtomicBoolean atomicBoolean = new AtomicBoolean(); + ReflectionUtils.doWithMethods(interfaceClass, method -> atomicBoolean.set(true), Method::isDefault); - return false; + return atomicBoolean.get(); } @Nullable diff --git a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java index efc13ca61..b9230771f 100644 --- a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.Jsr310Converters; +import org.springframework.data.util.Lazy; import org.springframework.data.util.NullableWrapperConverters; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -37,9 +38,9 @@ import org.springframework.util.ConcurrentReferenceHashMap; /** * A {@link ProjectionFactory} to create JDK proxies to back interfaces and handle method invocations on them. By - * default accessor methods are supported. In case the delegating lookups result in an object of different type that the - * projection interface method's return type, another projection will be created to transparently mitigate between the - * types. + * default, accessor methods are supported. In case the delegating lookups result in an object of different type that + * the projection interface method's return type, another projection will be created to transparently mitigate between + * the types. * * @author Oliver Gierke * @author Christoph Strobl @@ -59,9 +60,12 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware } private final List factories; - private final Map, ProjectionInformation> projectionInformationCache = new ConcurrentReferenceHashMap<>(); + private final Map, ProjectionMetadata> projectionInformationCache = new ConcurrentReferenceHashMap<>(); private @Nullable ClassLoader classLoader; + private final Lazy defaultMethodInvokingMethodInterceptor = Lazy + .of(DefaultMethodInvokingMethodInterceptor::new); + /** * Creates a new {@link ProxyProjectionFactory}. */ @@ -108,7 +112,12 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware factory.setOpaque(true); factory.setInterfaces(projectionType, TargetAware.class); - factory.addAdvice(new DefaultMethodInvokingMethodInterceptor()); + ProjectionMetadata projectionMetadata = getProjectionMetadata(projectionType); + + if (projectionMetadata.hasDefaultMethods) { + factory.addAdvice(defaultMethodInvokingMethodInterceptor.get()); + } + factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass())); factory.addAdvice(getMethodInterceptor(source, projectionType)); @@ -125,8 +134,12 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware @Override public final ProjectionInformation getProjectionInformation(Class projectionType) { + return getProjectionMetadata(projectionType).projectionInformation; + } - return projectionInformationCache.computeIfAbsent(projectionType, this::createProjectionInformation); + private ProjectionMetadata getProjectionMetadata(Class projectionType) { + return projectionInformationCache.computeIfAbsent(projectionType, + it -> ProjectionMetadata.create(it, createProjectionInformation(it))); } /** @@ -193,13 +206,11 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware * * @author Oliver Gierke */ - private static class TargetAwareMethodInterceptor implements MethodInterceptor { + private record TargetAwareMethodInterceptor(Class targetType) implements MethodInterceptor { private static final Method GET_TARGET_CLASS_METHOD; private static final Method GET_TARGET_METHOD; - private final Class targetType; - static { try { GET_TARGET_CLASS_METHOD = TargetAware.class.getMethod("getTargetClass"); @@ -214,10 +225,9 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware * * @param targetType must not be {@literal null}. */ - public TargetAwareMethodInterceptor(Class targetType) { + private TargetAwareMethodInterceptor { Assert.notNull(targetType, "Target type must not be null"); - this.targetType = targetType; } @Nullable @@ -239,7 +249,7 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware * * @author Oliver Gierke */ - private static enum MapAccessingMethodInterceptorFactory implements MethodInterceptorFactory { + private enum MapAccessingMethodInterceptorFactory implements MethodInterceptorFactory { INSTANCE; @@ -260,7 +270,7 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware * * @author Oliver Gierke */ - private static enum PropertyAccessingMethodInvokerFactory implements MethodInterceptorFactory { + private enum PropertyAccessingMethodInvokerFactory implements MethodInterceptorFactory { INSTANCE; @@ -274,4 +284,18 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware return true; } } + + /** + * Holder for {@link ProjectionInformation} and whether the target projection type uses {@code default} interface + * methods. + * + * @since 3.0.7 + */ + record ProjectionMetadata(boolean hasDefaultMethods, ProjectionInformation projectionInformation) { + + public static ProjectionMetadata create(Class projectionType, ProjectionInformation projectionInformation) { + return new ProjectionMetadata(DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(projectionType), + projectionInformation); + } + } } diff --git a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java index 0576646fc..285d742c3 100755 --- a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java @@ -28,7 +28,7 @@ import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; - +import org.springframework.aop.Advisor; import org.springframework.aop.TargetClassAware; import org.springframework.aop.framework.Advised; import org.springframework.test.util.ReflectionTestUtils; @@ -137,10 +137,10 @@ class ProxyProjectionFactoryUnitTests { assertThat(result).hasSize(6); } - @Test // DATACMNS-655 + @Test // DATACMNS-655, GH-2831 void invokesDefaultMethodOnProxy() { - var excerpt = factory.createProjection(CustomerExcerpt.class); + var excerpt = factory.createProjection(CustomerExcerptWithDefaultMethod.class); var advised = (Advised) ReflectionTestUtils.getField(Proxy.getInvocationHandler(excerpt), "advised"); var advisors = advised.getAdvisors(); @@ -149,6 +149,21 @@ class ProxyProjectionFactoryUnitTests { assertThat(advisors[0].getAdvice()).isInstanceOf(DefaultMethodInvokingMethodInterceptor.class); } + @Test // GH-2831 + void doesNotRegisterDefaultMethodInvokingMethodInterceptor() { + + var excerpt = factory.createProjection(CustomerExcerpt.class); + + var advised = (Advised) ReflectionTestUtils.getField(Proxy.getInvocationHandler(excerpt), "advised"); + var advisors = advised.getAdvisors(); + + assertThat(advisors.length).isGreaterThan(0); + + for (Advisor advisor : advisors) { + assertThat(advisor).isNotInstanceOf(DefaultMethodInvokingMethodInterceptor.class); + } + } + @Test // DATACMNS-648 void exposesProxyTarget() { @@ -271,8 +286,7 @@ class ProxyProjectionFactoryUnitTests { customer.address.city = "New York"; customer.address.zipCode = "ZIP"; - var excerpt = factory.createProjection(CustomerWithOptionalHavingProjection.class, - customer); + var excerpt = factory.createProjection(CustomerWithOptionalHavingProjection.class, customer); assertThat(excerpt.getFirstname()).isEqualTo("Dave"); assertThat(excerpt.getAddress()).hasValueSatisfying(addressExcerpt -> { @@ -343,6 +357,13 @@ class ProxyProjectionFactoryUnitTests { Map getData(); } + interface CustomerExcerptWithDefaultMethod extends CustomerExcerpt { + + default String getFirstnameAndId() { + return getFirstname() + " " + getId(); + } + } + interface AddressExcerpt { String getZipCode();