Add AOT support for Registry of HTTP Interface Proxies

This commit adds AOT support for restoring the state of the
HttpServiceProxyRegistry. This generates code for the groupsMetadata
as well as for the creation of the client proxies.

Closes gh-34750
This commit is contained in:
Stéphane Nicoll
2025-04-14 10:04:23 +02:00
parent e3e99ac8a0
commit 88e773ae24
9 changed files with 656 additions and 37 deletions

View File

@@ -21,20 +21,32 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
import org.springframework.web.service.registry.echo.EchoA;
import org.springframework.web.service.registry.echo.EchoB;
import org.springframework.web.service.registry.greeting.GreetingA;
import org.springframework.web.service.registry.greeting.GreetingB;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link AnnotationHttpServiceRegistrar}.
* Tests for {@link AnnotationHttpServiceRegistrar}.
*
* @author Rossen Stoyanchev
* @author Stephane Nicoll
*/
public class AnnotationHttpServiceRegistrarTests {
@@ -54,6 +66,19 @@ public class AnnotationHttpServiceRegistrarTests {
assertGroups(StubGroup.ofListing(ECHO_GROUP, EchoA.class, EchoB.class));
}
@Test
@CompileWithForkedClassLoader
void basicListingWithAot() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean(ListingConfig.class);
compile(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class);
assertThat(registry.getGroupNames()).containsOnly(ECHO_GROUP);
assertThat(registry.getClientTypesInGroup(ECHO_GROUP)).containsOnly(EchoA.class, EchoB.class);
});
}
@Test
void basicScan() {
doRegister(ScanConfig.class);
@@ -62,6 +87,20 @@ public class AnnotationHttpServiceRegistrarTests {
StubGroup.ofPackageClasses(GREETING_GROUP, GreetingA.class));
}
@Test
@CompileWithForkedClassLoader
void basicScanWithAot() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean(ScanConfig.class);
compile(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class);
assertThat(registry.getGroupNames()).containsOnly(ECHO_GROUP, GREETING_GROUP);
assertThat(registry.getClientTypesInGroup(ECHO_GROUP)).containsOnly(EchoA.class, EchoB.class);
assertThat(registry.getClientTypesInGroup(GREETING_GROUP)).containsOnly(GreetingA.class, GreetingB.class);
});
}
@Test
void clientType() {
doRegister(ClientTypeConfig.class);
@@ -75,6 +114,25 @@ public class AnnotationHttpServiceRegistrarTests {
this.registrar.registerHttpServices(this.groupRegistry, metadata);
}
@SuppressWarnings("unchecked")
private void compile(GenericApplicationContext applicationContext,
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(applicationContext, generationContext);
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile(compiled ->
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
}
private GenericApplicationContext toFreshApplicationContext(
ApplicationContextInitializer<GenericApplicationContext> initializer) {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
return freshApplicationContext;
}
private void assertGroups(StubGroup... expectedGroups) {
Map<String, StubGroup> groupMap = this.groupRegistry.groupMap();
assertThat(groupMap.size()).isEqualTo(expectedGroups.length);
@@ -88,18 +146,18 @@ public class AnnotationHttpServiceRegistrarTests {
}
@ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class, EchoB.class})
private static class ListingConfig {
@ImportHttpServices(group = ECHO_GROUP, types = { EchoA.class, EchoB.class })
static class ListingConfig {
}
@ImportHttpServices(group = ECHO_GROUP, basePackageClasses = {EchoA.class})
@ImportHttpServices(group = GREETING_GROUP, basePackageClasses = {GreetingA.class})
private static class ScanConfig {
@ImportHttpServices(group = ECHO_GROUP, basePackageClasses = { EchoA.class })
@ImportHttpServices(group = GREETING_GROUP, basePackageClasses = { GreetingA.class })
static class ScanConfig {
}
@ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = ECHO_GROUP, types = {EchoA.class})
@ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = {GreetingA.class})
private static class ClientTypeConfig {
@ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = ECHO_GROUP, types = { EchoA.class })
@ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = { GreetingA.class })
static class ClientTypeConfig {
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2002-2025 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.web.service.registry;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.BeanDefinitionPropertyValueCodeGeneratorDelegates;
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.service.registry.GroupsMetadata.DefaultRegistration;
import org.springframework.web.service.registry.GroupsMetadata.Registration;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
import org.springframework.web.service.registry.echo.EchoA;
import org.springframework.web.service.registry.echo.EchoB;
import org.springframework.web.service.registry.greeting.GreetingA;
import org.springframework.web.service.registry.greeting.GreetingB;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GroupsMetadataValueDelegate}.
*
* @author Stephane Nicoll
*/
@CompileWithForkedClassLoader
class GroupsMetadataValueDelegateTests {
@Test
void generateRegistrationWithOnlyName() {
DefaultRegistration registration = new DefaultRegistration("test", ClientType.UNSPECIFIED);
compile(registration, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(Registration.class, hasRegistration("test", ClientType.UNSPECIFIED)));
}
@Test
void generateRegistrationWitNoHttpServiceTypeName() {
DefaultRegistration registration = new DefaultRegistration("test", ClientType.REST_CLIENT);
compile(registration, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(Registration.class, hasRegistration("test", ClientType.REST_CLIENT)));
}
@Test
void generateRegistrationWitOneHttpServiceTypeName() {
DefaultRegistration registration = new DefaultRegistration("test", ClientType.WEB_CLIENT,
httpServiceTypeNames("com.example.MyClient"));
compile(registration, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(Registration.class, hasRegistration(
"test", ClientType.WEB_CLIENT, "com.example.MyClient")));
}
@Test
void generateRegistrationWitHttpServiceTypeNames() {
DefaultRegistration registration = new DefaultRegistration("test", ClientType.WEB_CLIENT,
httpServiceTypeNames("com.example.MyClient", "com.example.another.TestClient"));
compile(registration, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(Registration.class, hasRegistration(
"test", ClientType.WEB_CLIENT, "com.example.MyClient", "com.example.another.TestClient")));
}
@Test
void generateGroupsMetadataEmpty() {
compile(new GroupsMetadata(), (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()).isEmpty()));
}
@Test
void generateGroupsMetadataSingleGroup() {
GroupsMetadata groupsMetadata = new GroupsMetadata();
groupsMetadata.getOrCreateGroup("test-group", ClientType.REST_CLIENT).httpServiceTypeNames().add(EchoA.class.getName());
compile(groupsMetadata, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
.singleElement().satisfies(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class))));
}
@Test
void generateGroupsMetadataMultipleGroupsSimple() {
GroupsMetadata groupsMetadata = new GroupsMetadata();
groupsMetadata.getOrCreateGroup("test-group", ClientType.UNSPECIFIED).httpServiceTypeNames()
.addAll(List.of(EchoA.class.getName(), EchoB.class.getName()));
groupsMetadata.getOrCreateGroup("another-group", ClientType.WEB_CLIENT).httpServiceTypeNames()
.addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName()));
Function<GeneratedClass, ValueCodeGenerator> valueCodeGeneratorFactory = generatedClass ->
ValueCodeGenerator.withDefaults().add(List.of(new GroupsMetadataValueDelegate()));
compile(valueCodeGeneratorFactory, groupsMetadata, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
.hasSize(2)));
}
@Test
void generateGroupsMetadataMultipleGroups() {
GroupsMetadata groupsMetadata = new GroupsMetadata();
groupsMetadata.getOrCreateGroup("test-group", ClientType.UNSPECIFIED).httpServiceTypeNames()
.addAll(List.of(EchoA.class.getName(), EchoB.class.getName()));
groupsMetadata.getOrCreateGroup("another-group", ClientType.WEB_CLIENT).httpServiceTypeNames()
.addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName()));
compile(groupsMetadata, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
.hasSize(2)));
}
private LinkedHashSet<String> httpServiceTypeNames(String... names) {
return new LinkedHashSet<>(Arrays.asList(names));
}
private Consumer<Registration> hasRegistration(String name, ClientType clientType, String... httpServiceTypeNames) {
return registration -> {
assertThat(registration.name()).isEqualTo(name);
assertThat(registration.clientType()).isEqualTo(clientType);
assertThat(registration.httpServiceTypeNames()).isInstanceOf(LinkedHashSet.class)
.containsExactly(httpServiceTypeNames);
};
}
private Consumer<HttpServiceGroup> hasHttpServiceGroup(String name, ClientType clientType, Class<?>... httpServiceTypeNames) {
return group -> {
assertThat(group.name()).isEqualTo(name);
assertThat(group.clientType()).isEqualTo(clientType);
assertThat(group.httpServiceTypes()).containsOnly(httpServiceTypeNames);
};
}
private void compile(Function<GeneratedClass, ValueCodeGenerator> valueCodeGeneratorFactory,
Object value, BiConsumer<Object, Compiled> result) {
TestGenerationContext generationContext = new TestGenerationContext();
DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder();
GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeatureComponent("TestCode", GroupsMetadata.class, typeBuilder);
ValueCodeGenerator valueCodeGenerator = valueCodeGeneratorFactory.apply(generatedClass);
CodeBlock generatedCode = valueCodeGenerator.generateCode(value);
typeBuilder.set(type -> {
type.addModifiers(Modifier.PUBLIC);
type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(Object.class).addStatement("return $L", generatedCode).build());
});
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile(compiled ->
result.accept(getGeneratedCodeReturnValue(compiled, generatedClass), compiled));
}
private void compile(Object value, BiConsumer<Object, Compiled> result) {
compile(this::createValueCodeGenerator, value, result);
}
private ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) {
return BeanDefinitionPropertyValueCodeGeneratorDelegates.createValueCodeGenerator(
generatedClass.getMethods(), List.of(new GroupsMetadataValueDelegate()));
}
private static Object getGeneratedCodeReturnValue(Compiled compiled, GeneratedClass generatedClass) {
try {
Object instance = compiled.getInstance(Object.class, generatedClass.getName().reflectionName());
Method get = ReflectionUtils.findMethod(instance.getClass(), "get");
return get.invoke(null);
}
catch (Exception ex) {
throw new RuntimeException("Failed to invoke generated code '%s':".formatted(generatedClass.getName()), ex);
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2002-2025 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.web.service.registry;
import java.util.function.BiConsumer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.AotServices;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
import org.springframework.web.service.registry.echo.EchoA;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link HttpServiceProxyBeanRegistrationAotProcessor}.
*
* @author Stephane Nicoll
*/
class HttpServiceProxyRegistrationAotProcessorTests {
@Test
void httpServiceProxyBeanRegistrationAotProcessorIsRegistered() {
assertThat(AotServices.factories().load(BeanRegistrationAotProcessor.class))
.anyMatch(HttpServiceProxyBeanRegistrationAotProcessor.class::isInstance);
}
@Test
void getAotContributionWhenBeanHasNoGroup() {
assertThat(hasContribution(new RootBeanDefinition(EchoA.class))).isFalse();
}
@Test
void getAotContributionWhenBeanHasGroup() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class);
beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo");
assertThat(hasContribution(beanDefinition)).isTrue();
}
private boolean hasContribution(RootBeanDefinition beanDefinition) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("test", beanDefinition);
RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "test");
return new HttpServiceProxyBeanRegistrationAotProcessor().processAheadOfTime(registeredBean) != null;
}
@Test
@CompileWithForkedClassLoader
void processHttpServiceProxyWhenSingleClientType() {
GroupsMetadata groupsMetadata = new GroupsMetadata();
groupsMetadata.getOrCreateGroup("echo", ClientType.UNSPECIFIED)
.httpServiceTypeNames().add(EchoA.class.getName());
DefaultListableBeanFactory beanFactory = prepareBeanFactory(groupsMetadata);
RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class);
beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo");
beanFactory.registerBeanDefinition("echoA", beanDefinition);
compile(beanFactory, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class);
assertThat(registry.getClient("echo", EchoA.class)).isSameAs(freshApplicationContext.getBean(EchoA.class));
});
}
@Test
@CompileWithForkedClassLoader
void processHttpServiceProxyWhenSameClientTypeInDifferentGroups() {
GroupsMetadata groupsMetadata = new GroupsMetadata();
groupsMetadata.getOrCreateGroup("echo", ClientType.UNSPECIFIED)
.httpServiceTypeNames().add(EchoA.class.getName());
groupsMetadata.getOrCreateGroup("echo2", ClientType.UNSPECIFIED)
.httpServiceTypeNames().add(EchoA.class.getName());
DefaultListableBeanFactory beanFactory = prepareBeanFactory(groupsMetadata);
RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class);
beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo");
beanFactory.registerBeanDefinition("echoA", beanDefinition);
RootBeanDefinition beanDefinition2 = new RootBeanDefinition(EchoA.class);
beanDefinition2.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo2");
beanFactory.registerBeanDefinition("echoA2", beanDefinition2);
compile(beanFactory, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class);
assertThat(registry.getClient("echo", EchoA.class)).isSameAs(freshApplicationContext.getBean("echoA", EchoA.class));
assertThat(registry.getClient("echo2", EchoA.class)).isSameAs(freshApplicationContext.getBean("echoA2", EchoA.class));
});
}
private DefaultListableBeanFactory prepareBeanFactory(GroupsMetadata metadata) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
RootBeanDefinition beanDefinition = new RootBeanDefinition(HttpServiceProxyRegistryFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, metadata);
beanFactory.registerBeanDefinition(AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, beanDefinition);
return beanFactory;
}
@SuppressWarnings("unchecked")
private void compile(DefaultListableBeanFactory beanFactory,
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(new GenericApplicationContext(beanFactory), generationContext);
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile(compiled ->
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
}
private GenericApplicationContext toFreshApplicationContext(
ApplicationContextInitializer<GenericApplicationContext> initializer) {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
return freshApplicationContext;
}
}

View File

@@ -149,7 +149,7 @@ public class HttpServiceRegistrarTests {
}
private Map<String, HttpServiceGroup> groupMap() {
BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition("httpServiceProxyRegistry");
BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition(AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME);
assertThat(beanDef.getBeanClassName()).isEqualTo(HttpServiceProxyRegistryFactoryBean.class.getName());
ConstructorArgumentValues args = beanDef.getConstructorArgumentValues();