From 6207db09907e75a0a4185454ac6c58f49abba997 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 17 Jul 2024 13:31:48 +0200 Subject: [PATCH] Generate proxy and register reflection for default sorting during AOT. This commit makes sure to generate required cglib proxies during AOT phase so they are ready to use within a native image. Prior to this change default sorting raised an error as class generation is not allowed in a GraalVM native image. Original pull request: #4747 Closes #4744 --- .../aot/LazyLoadingProxyAotProcessor.java | 4 +- .../mongodb/aot/MongoAotReflectionHelper.java | 31 +++++++++ .../aot/RepositoryRuntimeHints.java | 16 +++++ .../mongodb/repository/query/QueryUtils.java | 66 ++++++++++++++----- 4 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java index 09080c32d5..530ffce510 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java @@ -24,7 +24,6 @@ import java.util.Set; import org.springframework.aot.generate.GenerationContext; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotations; @@ -77,8 +76,7 @@ public void registerLazyLoadingProxyIfNeeded(Class type, GenerationContext ge LazyLoadingInterceptor::none); // see: spring-projects/spring-framework/issues/29309 - generationContext.getRuntimeHints().reflection().registerType(proxyClass, - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS); + generationContext.getRuntimeHints().reflection().registerType(proxyClass, MongoAotReflectionHelper::cglibProxyReflectionMemberAccess); } }); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java new file mode 100644 index 0000000000..e3d3a30336 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.mongodb.aot; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeHint.Builder; + +/** + * @author Christoph Strobl + */ +public final class MongoAotReflectionHelper { + + public static void cglibProxyReflectionMemberAccess(Builder builder) { + + builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.DECLARED_FIELDS); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java index 203e5e9810..8ed6ea66c4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java @@ -24,6 +24,10 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.data.mongodb.aot.MongoAotPredicates; +import org.springframework.data.mongodb.aot.MongoAotReflectionHelper; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.QueryUtils; import org.springframework.data.mongodb.repository.support.CrudMethodMetadata; import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor; @@ -45,6 +49,8 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); + registerHintsForDefaultSorting(hints, classLoader); + if (isAopPresent(classLoader)) { // required for pushing ReadPreference,... into the default repository implementation @@ -92,4 +98,14 @@ private static void registerQuerydslHints(RuntimeHints hints, @Nullable ClassLoa private static boolean isAopPresent(@Nullable ClassLoader classLoader) { return ClassUtils.isPresent("org.springframework.aop.Pointcut", classLoader); } + + private static void registerHintsForDefaultSorting(RuntimeHints hints, @Nullable ClassLoader classLoader) { + + List types = List.of(TypeReference.of(Query.class), // + TypeReference.of(QueryUtils.queryProxyType(Query.class, classLoader)), // + TypeReference.of(BasicQuery.class), // + TypeReference.of(QueryUtils.queryProxyType(BasicQuery.class, classLoader))); + + hints.reflection().registerTypes(types, MongoAotReflectionHelper::cglibProxyReflectionMemberAccess); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index 1aab34055a..c7cb84d091 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -19,12 +19,16 @@ import java.util.List; import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.bson.Document; import org.springframework.aop.framework.ProxyFactory; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.ExpressionParser; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -37,7 +41,9 @@ * @since 2.1 * @currentRead Assassin's Apprentice - Robin Hobb */ -class QueryUtils { +public class QueryUtils { + + protected static final Log LOGGER = LogFactory.getLog(QueryUtils.class); /** * Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given @@ -47,28 +53,27 @@ class QueryUtils { * @param defaultSort the default sort expression to apply to the query. * @return the query having the given {@code sort} applied. */ - static Query decorateSort(Query query, Document defaultSort) { + public static Query decorateSort(Query query, Document defaultSort) { if (defaultSort.isEmpty()) { return query; } - ProxyFactory factory = new ProxyFactory(query); - factory.addAdvice((MethodInterceptor) invocation -> { - - if (!invocation.getMethod().getName().equals("getSortObject")) { - return invocation.proceed(); - } - - Document combinedSort = new Document(defaultSort); - combinedSort.putAll((Document) invocation.proceed()); - return combinedSort; - }); - factory.setInterfaces(new Class[0]); - + ProxyFactory factory = prepareQueryProxy(query.getClass(), defaultSort); + factory.setTarget(query); return (Query) factory.getProxy(query.getClass().getClassLoader()); } + /** + * Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given + * {@code sort} may be overwritten by the sort explicitly defined by the {@link Query} itself. + * + * @param classLoader the {@link ClassLoader} to use for generating the proxy type with. + */ + public static Class queryProxyType(Class baseType, ClassLoader classLoader) { + return prepareQueryProxy(baseType, new Document()).getProxyClass(classLoader); + } + /** * Apply a collation extracted from the given {@literal collationExpression} to the given {@link Query}. Potentially * replace parameter placeholders with values from the {@link ConvertingParameterAccessor accessor}. @@ -124,4 +129,35 @@ static int indexOfAssignableParameter(Class type, List> parameters) } return -1; } + + private static ProxyFactory prepareQueryProxy(Class query, Document defaultSort) { + + ProxyFactory factory = new ProxyFactory(); + factory.setTargetClass(query); + factory.addAdvice(new DefaultSortingInterceptor(defaultSort)); + factory.setInterfaces(new Class[0]); + return factory; + } + + static class DefaultSortingInterceptor implements MethodInterceptor { + + private final Document defaultSort; + + public DefaultSortingInterceptor(Document defaultSort) { + this.defaultSort = defaultSort; + } + + @Nullable + @Override + public Object invoke(@NonNull MethodInvocation invocation) throws Throwable { + + if (!invocation.getMethod().getName().equals("getSortObject")) { + return invocation.proceed(); + } + + Document combinedSort = new Document(defaultSort); + combinedSort.putAll((Document) invocation.proceed()); + return combinedSort; + } + } }