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

@ -26,7 +26,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@ -38,7 +38,6 @@ import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.service.annotation.HttpExchange;
/**
@ -69,6 +68,7 @@ import org.springframework.web.service.annotation.HttpExchange;
* @author Rossen Stoyanchev
* @author Phillip Webb
* @author Olga Maciaszek-Sharma
* @author Stephane Nicoll
* @since 7.0
* @see ImportHttpServices
* @see HttpServiceProxyRegistryFactoryBean
@ -76,6 +76,13 @@ import org.springframework.web.service.annotation.HttpExchange;
public abstract class AbstractHttpServiceRegistrar implements
ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware {
/**
* The bean name of the {@link HttpServiceProxyRegistry}.
*/
public static final String HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME = "httpServiceProxyRegistry";
static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName";
private HttpServiceGroup.ClientType defaultClientType = HttpServiceGroup.ClientType.UNSPECIFIED;
private @Nullable Environment environment;
@ -127,33 +134,36 @@ public abstract class AbstractHttpServiceRegistrar implements
registerHttpServices(new DefaultGroupRegistry(), metadata);
String proxyRegistryBeanName = StringUtils.uncapitalize(HttpServiceProxyRegistry.class.getSimpleName());
GenericBeanDefinition proxyRegistryBeanDef;
if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) {
proxyRegistryBeanDef = new GenericBeanDefinition();
proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class);
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
args.addIndexedArgumentValue(0, new GroupsMetadata());
beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef);
}
else {
proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName);
}
RootBeanDefinition proxyRegistryBeanDef = createOrGetRegistry(beanRegistry);
mergeGroups(proxyRegistryBeanDef);
this.groupsMetadata.forEachRegistration((groupName, types) -> types.forEach(type -> {
GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition();
RootBeanDefinition proxyBeanDef = new RootBeanDefinition();
proxyBeanDef.setBeanClassName(type);
proxyBeanDef.setAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, groupName);
proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(groupName, type));
String beanName = (groupName + "#" + type);
proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type));
if (!beanRegistry.containsBeanDefinition(beanName)) {
beanRegistry.registerBeanDefinition(beanName, proxyBeanDef);
}
}));
}
private RootBeanDefinition createOrGetRegistry(BeanDefinitionRegistry beanRegistry) {
if (!beanRegistry.containsBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME)) {
RootBeanDefinition proxyRegistryBeanDef = new RootBeanDefinition();
proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class);
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
args.addIndexedArgumentValue(0, new GroupsMetadata());
beanRegistry.registerBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, proxyRegistryBeanDef);
return proxyRegistryBeanDef;
}
else {
return (RootBeanDefinition) beanRegistry.getBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME);
}
}
/**
* This method is called before any bean definition registrations are made.
* Subclasses must implement it to register the HTTP Services for which bean
@ -175,7 +185,7 @@ public abstract class AbstractHttpServiceRegistrar implements
return this.scanner;
}
private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) {
private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) {
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, GroupsMetadata.class);
Assert.state(valueHolder != null, "Expected GroupsMetadata constructor argument at index 0");
@ -184,12 +194,10 @@ public abstract class AbstractHttpServiceRegistrar implements
target.mergeWith(this.groupsMetadata);
}
private Object getProxyInstance(String registryBeanName, String groupName, String httpServiceType) {
private Object getProxyInstance(String groupName, String httpServiceType) {
Assert.state(this.beanFactory != null, "BeanFactory has not been set");
HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class);
Object proxy = registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType));
Assert.notNull(proxy, "No proxy for HTTP Service [" + httpServiceType + "]");
return proxy;
HttpServiceProxyRegistry registry = this.beanFactory.getBean(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, HttpServiceProxyRegistry.class);
return registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType));
}

View File

@ -17,12 +17,14 @@
package org.springframework.web.service.registry;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -37,8 +39,16 @@ import org.springframework.util.ClassUtils;
*/
final class GroupsMetadata {
private final Map<String, DefaultRegistration> groupMap = new LinkedHashMap<>();
private final Map<String, DefaultRegistration> groupMap;
public GroupsMetadata() {
this(Collections.emptyList());
}
GroupsMetadata(Iterable<DefaultRegistration> registrations) {
this.groupMap = new LinkedHashMap<>();
registrations.forEach(registration -> this.groupMap.put(registration.name(), registration));
}
/**
* Create a registration for the given group name, or return an existing
@ -85,6 +95,13 @@ final class GroupsMetadata {
}
}
/**
* Return the raw {@link DefaultRegistration registrations}.
*/
Stream<DefaultRegistration> registrations() {
return this.groupMap.values().stream();
}
/**
* Registration metadata for an {@link HttpServiceGroup}.
@ -102,17 +119,22 @@ final class GroupsMetadata {
/**
* Default implementation of {@link Registration}.
*/
private static class DefaultRegistration implements Registration {
static class DefaultRegistration implements Registration {
private final String name;
private HttpServiceGroup.ClientType clientType;
private final Set<String> typeNames = new LinkedHashSet<>();
private final Set<String> typeNames;
DefaultRegistration(String name, HttpServiceGroup.ClientType clientType) {
this(name, clientType, new LinkedHashSet<>());
}
DefaultRegistration(String name, HttpServiceGroup.ClientType clientType, Set<String> typeNames) {
this.name = name;
this.clientType = clientType;
this.typeNames = typeNames;
}
@Override

View File

@ -0,0 +1,94 @@
/*
* 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.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.javapoet.CodeBlock;
import org.springframework.web.service.registry.GroupsMetadata.DefaultRegistration;
/**
* {@link ValueCodeGenerator.Delegate} for {@link GroupsMetadata}.
*
* @author Stephane Nicoll
* @since 7.0
*/
final class GroupsMetadataValueDelegate implements ValueCodeGenerator.Delegate {
@Override
public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof DefaultRegistration registration) {
return generateRegistrationCode(valueCodeGenerator, registration);
}
if (value instanceof GroupsMetadata groupsMetadata) {
return generateGroupsMetadataCode(valueCodeGenerator, groupsMetadata);
}
return null;
}
public CodeBlock generateRegistrationCode(ValueCodeGenerator
valueCodeGenerator, DefaultRegistration value) {
CodeBlock.Builder code = CodeBlock.builder();
code.add("new $T($S, $L, $L)", DefaultRegistration.class, value.name(),
valueCodeGenerator.generateCode(value.clientType()),
!value.httpServiceTypeNames().isEmpty() ?
valueCodeGenerator.generateCode(value.httpServiceTypeNames()) :
CodeBlock.of("new $T()", LinkedHashSet.class));
return code.build();
}
private CodeBlock generateGroupsMetadataCode(ValueCodeGenerator valueCodeGenerator, GroupsMetadata groupsMetadata) {
Collection<DefaultRegistration> registrations = groupsMetadata.registrations()
.collect(Collectors.toCollection(ArrayList::new));
if (valueCodeGenerator.getGeneratedMethods() != null) {
return valueCodeGenerator.getGeneratedMethods().add("getGroupsMetadata", method -> method
.addJavadoc("Create the {@link $T}.", GroupsMetadata.class)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(GroupsMetadata.class)
.addCode(generateGroupsMetadataMethod(valueCodeGenerator, registrations))).toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none());
}
else {
return CodeBlock.of("new $T($L)", GroupsMetadata.class, valueCodeGenerator.generateCode(registrations));
}
}
private CodeBlock generateGroupsMetadataMethod(
ValueCodeGenerator valueCodeGenerator, Collection<DefaultRegistration> registrations) {
CodeBlock.Builder code = CodeBlock.builder();
String registrationsVariable = "registrations";
code.addStatement("$T<$T> $L = new $T<>()", List.class, DefaultRegistration.class,
registrationsVariable, ArrayList.class);
registrations.forEach(registration ->
code.addStatement("$L.add($L)", registrationsVariable,
valueCodeGenerator.generateCode(registration))
);
code.addStatement("return new $T($L)", GroupsMetadata.class, registrationsVariable);
return code.build();
}
}

View File

@ -0,0 +1,94 @@
/*
* 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 javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
import org.springframework.beans.factory.support.InstanceSupplier;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE;
import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME;
/**
* {@link BeanRegistrationAotProcessor} for HTTP service proxy support.
*
* @author Stephane Nicoll
* @see AbstractHttpServiceRegistrar
*/
final class HttpServiceProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
@Override
public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
Object value = registeredBean.getMergedBeanDefinition().getAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE);
if (value instanceof String groupName) {
return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments ->
new HttpServiceProxyRegistrationCodeFragments(codeFragments, groupName, registeredBean.getBeanClass()));
}
return null;
}
private static class HttpServiceProxyRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator {
private static final String REGISTERED_BEAN_PARAMETER = "registeredBean";
private final String groupName;
private final Class<?> clientType;
HttpServiceProxyRegistrationCodeFragments(BeanRegistrationCodeFragments delegate,
String groupName, Class<?> clientType) {
super(delegate);
this.groupName = groupName;
this.clientType = clientType;
}
@Override
public ClassName getTarget(RegisteredBean registeredBean) {
return ClassName.get(registeredBean.getBeanClass());
}
@Override
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods()
.add("getHttpServiceProxy", method -> {
method.addJavadoc("Create the HTTP service proxy for {@link $T} and group {@code $L}.",
this.clientType, this.groupName);
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER);
method.returns(Object.class);
method.addStatement("return $L.getBeanFactory().getBean($S, $T.class).getClient($S, $T.class)",
REGISTERED_BEAN_PARAMETER, HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME,
HttpServiceProxyRegistry.class, this.groupName, this.clientType);
});
return CodeBlock.of("$T.of($L)", InstanceSupplier.class, generatedMethod.toMethodReference().toCodeBlock());
}
}
}

View File

@ -6,4 +6,8 @@ org.springframework.http.converter.json.ProblemDetailRuntimeHints,\
org.springframework.web.util.WebUtilRuntimeHints
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor
org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor,\
org.springframework.web.service.registry.HttpServiceProxyBeanRegistrationAotProcessor
org.springframework.aot.generate.ValueCodeGenerator$Delegate=\
org.springframework.web.service.registry.GroupsMetadataValueDelegate

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();