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; @@ -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 { @@ -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})
* <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}.
*

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

@ -15,14 +15,16 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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.
*/

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

@ -21,16 +21,15 @@ import java.util.List; @@ -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; @@ -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 { @@ -61,8 +61,8 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext {
}
@Override
public Environment getEnvironment() {
return null;
public MockEnvironment getEnvironment() {
return environment;
}
@Override

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

@ -15,9 +15,11 @@ @@ -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; @@ -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 { @@ -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<? 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
void callsMethodContributionForQueryMethod() {
@ -175,7 +254,6 @@ class RepositoryContributorUnitTests { @@ -175,7 +254,6 @@ class RepositoryContributorUnitTests {
contributor.contribute(testGenerationContext);
testGenerationContext.writeGeneratedContent();
contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findByFirstname");
}

Loading…
Cancel
Save