Browse Source

Avoid attempts to override AOT generated query method metadata.

Prior to this change regenerating repository instances for eg. test execution caused trouble when trying to override existing json metadata files. We now back off in case of existing files and added an explicit config flag for users to opt out of having the metadata file being present in the target resources.

Original pull request: #3355
Closes #3354
pull/3356/head
Christoph Strobl 4 months ago committed by Mark Paluch
parent
commit
8f34f0fbe9
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 14
      src/main/java/org/springframework/data/aot/AotContext.java
  2. 49
      src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java
  3. 8
      src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java
  4. 86
      src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java

14
src/main/java/org/springframework/data/aot/AotContext.java

@ -54,6 +54,7 @@ import org.springframework.util.StringUtils;
public interface AotContext extends EnvironmentCapable { public interface AotContext extends EnvironmentCapable {
String GENERATED_REPOSITORIES_ENABLED = "spring.aot.repositories.enabled"; 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}. * 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); 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})
* <p>
* 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}. * Returns a reference to the {@link ConfigurableListableBeanFactory} backing this {@link AotContext}.
* *

49
src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java

@ -15,14 +15,16 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.generate.GeneratedTypeReference; import org.springframework.aot.generate.GeneratedTypeReference;
import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory; 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 Log logger = LogFactory.getLog(RepositoryContributor.class);
private static final String FEATURE_NAME = "AotRepository"; private static final String FEATURE_NAME = "AotRepository";
private final AotRepositoryContext repositoryContext;
private final AotRepositoryCreator creator; private final AotRepositoryCreator creator;
private @Nullable TypeReference contributedTypeName; private @Nullable TypeReference contributedTypeName;
@ -59,6 +62,7 @@ public class RepositoryContributor {
*/ */
public RepositoryContributor(AotRepositoryContext repositoryContext) { public RepositoryContributor(AotRepositoryContext repositoryContext) {
this.repositoryContext = repositoryContext;
creator = AotRepositoryCreator.forRepository(repositoryContext.getRepositoryInformation(), creator = AotRepositoryCreator.forRepository(repositoryContext.getRepositoryInformation(),
repositoryContext.getModuleName(), createProjectionFactory()); repositoryContext.getModuleName(), createProjectionFactory());
} }
@ -139,20 +143,18 @@ public class RepositoryContributor {
// write out the content // write out the content
AotBundle aotBundle = creator.create(targetTypeSpec); 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()) { if (logger.isTraceEnabled()) {
logger.trace(""" if (repositoryContext.isGeneratedRepositoriesMetadataEnabled()) {
------ AOT Repository.json: %s ------ logger.trace("""
%s ------ AOT Repository.json: %s ------
------------------- %s
""".formatted(aotBundle.repositoryJsonFileName(), repositoryJson)); -------------------
""".formatted(aotBundle.repositoryJsonFileName(), repositoryJson));
}
TypeSpec typeSpec = targetTypeSpec.build(); TypeSpec typeSpec = targetTypeSpec.build();
JavaFile javaFile = JavaFile.builder(creator.packageName(), typeSpec).build(); JavaFile javaFile = JavaFile.builder(creator.packageName(), typeSpec).build();
@ -164,7 +166,14 @@ public class RepositoryContributor {
""".formatted(typeSpec.name(), javaFile)); """.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 // generate native runtime hints
@ -176,6 +185,20 @@ public class RepositoryContributor {
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); 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. * Customization hook for store implementations to customize class after building the entire class.
*/ */

8
src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java

@ -21,16 +21,15 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.test.tools.ClassFile; import org.springframework.core.test.tools.ClassFile;
import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.mock.env.MockEnvironment;
/** /**
* Dummy {@link AotRepositoryContext} used to simulate module specific repository implementation. * 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 { class DummyModuleAotRepositoryContext implements AotRepositoryContext {
private final StubRepositoryInformation repositoryInformation; private final StubRepositoryInformation repositoryInformation;
private final MockEnvironment environment = new MockEnvironment();
public DummyModuleAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { public DummyModuleAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
@ -61,8 +61,8 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext {
} }
@Override @Override
public Environment getEnvironment() { public MockEnvironment getEnvironment() {
return null; return environment;
} }
@Override @Override

86
src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java

@ -15,9 +15,11 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import example.UserRepository; import example.UserRepository;
import example.UserRepositoryExtension; import example.UserRepositoryExtension;
@ -33,6 +35,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.core.test.tools.ResourceFile;
import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.aot.CodeContributionAssert; import org.springframework.data.aot.CodeContributionAssert;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
@ -137,6 +140,82 @@ class RepositoryContributorUnitTests {
new CodeContributionAssert(generationContext).contributesReflectionFor(expectedTypeName); 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<? extends QueryMethod> 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<? extends QueryMethod> 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 @Test // GH-3279
void callsMethodContributionForQueryMethod() { void callsMethodContributionForQueryMethod() {
@ -175,7 +254,6 @@ class RepositoryContributorUnitTests {
contributor.contribute(testGenerationContext); contributor.contribute(testGenerationContext);
testGenerationContext.writeGeneratedContent(); testGenerationContext.writeGeneratedContent();
contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findByFirstname"); contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findByFirstname");
} }

Loading…
Cancel
Save