diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index 67f423ae6..0f9a0a5c8 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -54,6 +54,7 @@ import org.springframework.util.StringUtils; public interface AotContext extends EnvironmentCapable { String GENERATED_REPOSITORIES_ENABLED = "spring.aot.repositories.enabled"; + String GENERATED_REPOSITORIES_JSON_ENABLED = "spring.aot.repositories.metadata.enabled"; /** * Create an {@link AotContext} backed by the given {@link BeanFactory}. @@ -116,6 +117,19 @@ public interface AotContext extends EnvironmentCapable { return environment.getProperty(modulePropertyName, Boolean.class, true); } + /** + * Checks if repository metadata file writing is enabled by checking environment variables for general + * enablement ({@link #GENERATED_REPOSITORIES_JSON_ENABLED}) + *

+ * Unset properties are considered being {@literal true}. + * + * @return indicator if repository metadata should be written + * @since 5.0 + */ + default boolean isGeneratedRepositoriesMetadataEnabled() { + return getEnvironment().getProperty(GENERATED_REPOSITORIES_JSON_ENABLED, Boolean.class, true); + } + /** * Returns a reference to the {@link ConfigurableListableBeanFactory} backing this {@link AotContext}. * diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index 2ed9945df..9eed36fb1 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -15,14 +15,16 @@ */ package org.springframework.data.repository.aot.generate; +import java.io.ByteArrayInputStream; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.Collections; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.GeneratedTypeReference; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; @@ -49,6 +51,7 @@ public class RepositoryContributor { private static final Log logger = LogFactory.getLog(RepositoryContributor.class); private static final String FEATURE_NAME = "AotRepository"; + private final AotRepositoryContext repositoryContext; private final AotRepositoryCreator creator; private @Nullable TypeReference contributedTypeName; @@ -59,6 +62,7 @@ public class RepositoryContributor { */ public RepositoryContributor(AotRepositoryContext repositoryContext) { + this.repositoryContext = repositoryContext; creator = AotRepositoryCreator.forRepository(repositoryContext.getRepositoryInformation(), repositoryContext.getModuleName(), createProjectionFactory()); } @@ -139,20 +143,18 @@ public class RepositoryContributor { // write out the content AotBundle aotBundle = creator.create(targetTypeSpec); - String repositoryJson; - try { - repositoryJson = aotBundle.metadata().get().toJson().toString(2); - } catch (JSONException e) { - throw new RuntimeException(e); - } + String repositoryJson = generateJsonMetadata(aotBundle); + if (logger.isTraceEnabled()) { - logger.trace(""" - ------ AOT Repository.json: %s ------ - %s - ------------------- - """.formatted(aotBundle.repositoryJsonFileName(), repositoryJson)); + if (repositoryContext.isGeneratedRepositoriesMetadataEnabled()) { + logger.trace(""" + ------ AOT Repository.json: %s ------ + %s + ------------------- + """.formatted(aotBundle.repositoryJsonFileName(), repositoryJson)); + } TypeSpec typeSpec = targetTypeSpec.build(); JavaFile javaFile = JavaFile.builder(creator.packageName(), typeSpec).build(); @@ -164,7 +166,14 @@ public class RepositoryContributor { """.formatted(typeSpec.name(), javaFile)); } - generationContext.getGeneratedFiles().addResourceFile(aotBundle.repositoryJsonFileName(), repositoryJson); + if (repositoryContext.isGeneratedRepositoriesMetadataEnabled()) { + generationContext.getGeneratedFiles().handleFile(Kind.RESOURCE, aotBundle.repositoryJsonFileName(), + fileHandler -> { + if (!fileHandler.exists()) { + fileHandler.create(() -> new ByteArrayInputStream(repositoryJson.getBytes(StandardCharsets.UTF_8))); + } + }); + } }); // generate native runtime hints @@ -176,6 +185,20 @@ public class RepositoryContributor { MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); } + private String generateJsonMetadata(AotBundle aotBundle) { + + String repositoryJson = ""; + + if (repositoryContext.isGeneratedRepositoriesMetadataEnabled()) { + try { + repositoryJson = aotBundle.metadata().get().toJson().toString(2); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return repositoryJson; + } + /** * Customization hook for store implementations to customize class after building the entire class. */ diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java index 3d3b3ffc6..ff578c394 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -21,16 +21,15 @@ import java.util.List; import java.util.Set; import org.jspecify.annotations.Nullable; - import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.test.tools.ClassFile; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.mock.env.MockEnvironment; /** * Dummy {@link AotRepositoryContext} used to simulate module specific repository implementation. @@ -40,6 +39,7 @@ import org.springframework.data.repository.core.support.RepositoryComposition; class DummyModuleAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; + private final MockEnvironment environment = new MockEnvironment(); public DummyModuleAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); @@ -61,8 +61,8 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { } @Override - public Environment getEnvironment() { - return null; + public MockEnvironment getEnvironment() { + return environment; } @Override diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java index efe4a741e..a30a5ba2e 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java @@ -15,9 +15,11 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import example.UserRepository; import example.UserRepositoryExtension; @@ -33,6 +35,7 @@ import java.util.Set; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.ResourceFile; import org.springframework.core.test.tools.TestCompiler; import org.springframework.data.aot.CodeContributionAssert; import org.springframework.data.repository.CrudRepository; @@ -137,6 +140,82 @@ class RepositoryContributorUnitTests { new CodeContributionAssert(generationContext).contributesReflectionFor(expectedTypeName); } + @Test // GH-3354 + void doesNotWriteCapturedQueryMetadataToResourcesIfDisabled() { + + DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); + aotContext.getEnvironment().setProperty("spring.aot.repositories.metadata.enabled", "false"); + + RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { + + @Override + protected @Nullable MethodContributor contributeQueryMethod(Method method) { + + return MethodContributor + .forQueryMethod( + new QueryMethod(method, getRepositoryInformation(), getProjectionFactory(), DefaultParameters::new)) + .withMetadata(() -> Map.of("filter", "FILTER(%s > $1)".formatted(method.getName()), "project", + Arrays.stream(method.getParameters()).map(Parameter::getName).toList())) + .contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + if (!ClassUtils.isVoidType(method.getReturnType())) { + builder.addStatement("return null"); + } + + return builder.build(); + }); + } + }; + + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + repositoryContributor.contribute(generationContext); + generationContext.writeGeneratedContent(); + + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + assertThat(compiled.getResourceFiles()).isEmpty(); + }); + } + + @Test // GH-3354 + void doesNotWriteCapturedQueryMetadataToResourcesIfAlreadyExists() { + + DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); + + RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { + + @Override + protected @Nullable MethodContributor contributeQueryMethod(Method method) { + + return MethodContributor + .forQueryMethod( + new QueryMethod(method, getRepositoryInformation(), getProjectionFactory(), DefaultParameters::new)) + .withMetadata(() -> Map.of("filter", "FILTER(%s > $1)".formatted(method.getName()), "project", + Arrays.stream(method.getParameters()).map(Parameter::getName).toList())) + .contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + if (!ClassUtils.isVoidType(method.getReturnType())) { + builder.addStatement("return null"); + } + + return builder.build(); + }); + } + }; + + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + repositoryContributor.contribute(generationContext); + generationContext.writeGeneratedContent(); + + ResourceFile rf = ResourceFile.of(UserRepository.class.getName().replace('.', '/') + ".json", + "But you're untouchable, burning brighter than the sun"); + TestCompiler.forSystem().with(generationContext).withResources(rf).compile(compiled -> { + String content = compiled.getResourceFile().getContent(); + assertThat(content).contains("you're untouchable").doesNotContain("FILTER(doSomething > $1)"); + }); + } + @Test // GH-3279 void callsMethodContributionForQueryMethod() { @@ -175,7 +254,6 @@ class RepositoryContributorUnitTests { contributor.contribute(testGenerationContext); testGenerationContext.writeGeneratedContent(); - contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findByFirstname"); }