Skip to content

Commit 88e773a

Browse files
committed
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
1 parent e3e99ac commit 88e773a

9 files changed

+656
-37
lines changed

Diff for: spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java

+31-23
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import org.springframework.beans.factory.config.ConstructorArgumentValues;
2727
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2828
import org.springframework.beans.factory.support.BeanNameGenerator;
29-
import org.springframework.beans.factory.support.GenericBeanDefinition;
29+
import org.springframework.beans.factory.support.RootBeanDefinition;
3030
import org.springframework.context.EnvironmentAware;
3131
import org.springframework.context.ResourceLoaderAware;
3232
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@@ -38,7 +38,6 @@
3838
import org.springframework.core.type.classreading.MetadataReader;
3939
import org.springframework.core.type.filter.AnnotationTypeFilter;
4040
import org.springframework.util.Assert;
41-
import org.springframework.util.StringUtils;
4241
import org.springframework.web.service.annotation.HttpExchange;
4342

4443
/**
@@ -69,13 +68,21 @@
6968
* @author Rossen Stoyanchev
7069
* @author Phillip Webb
7170
* @author Olga Maciaszek-Sharma
71+
* @author Stephane Nicoll
7272
* @since 7.0
7373
* @see ImportHttpServices
7474
* @see HttpServiceProxyRegistryFactoryBean
7575
*/
7676
public abstract class AbstractHttpServiceRegistrar implements
7777
ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware {
7878

79+
/**
80+
* The bean name of the {@link HttpServiceProxyRegistry}.
81+
*/
82+
public static final String HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME = "httpServiceProxyRegistry";
83+
84+
static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName";
85+
7986
private HttpServiceGroup.ClientType defaultClientType = HttpServiceGroup.ClientType.UNSPECIFIED;
8087

8188
private @Nullable Environment environment;
@@ -127,33 +134,36 @@ public final void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefin
127134

128135
registerHttpServices(new DefaultGroupRegistry(), metadata);
129136

130-
String proxyRegistryBeanName = StringUtils.uncapitalize(HttpServiceProxyRegistry.class.getSimpleName());
131-
GenericBeanDefinition proxyRegistryBeanDef;
132-
133-
if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) {
134-
proxyRegistryBeanDef = new GenericBeanDefinition();
135-
proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class);
136-
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
137-
args.addIndexedArgumentValue(0, new GroupsMetadata());
138-
beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef);
139-
}
140-
else {
141-
proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName);
142-
}
137+
RootBeanDefinition proxyRegistryBeanDef = createOrGetRegistry(beanRegistry);
143138

144139
mergeGroups(proxyRegistryBeanDef);
145140

146141
this.groupsMetadata.forEachRegistration((groupName, types) -> types.forEach(type -> {
147-
GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition();
142+
RootBeanDefinition proxyBeanDef = new RootBeanDefinition();
148143
proxyBeanDef.setBeanClassName(type);
144+
proxyBeanDef.setAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, groupName);
145+
proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(groupName, type));
149146
String beanName = (groupName + "#" + type);
150-
proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type));
151147
if (!beanRegistry.containsBeanDefinition(beanName)) {
152148
beanRegistry.registerBeanDefinition(beanName, proxyBeanDef);
153149
}
154150
}));
155151
}
156152

153+
private RootBeanDefinition createOrGetRegistry(BeanDefinitionRegistry beanRegistry) {
154+
if (!beanRegistry.containsBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME)) {
155+
RootBeanDefinition proxyRegistryBeanDef = new RootBeanDefinition();
156+
proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class);
157+
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
158+
args.addIndexedArgumentValue(0, new GroupsMetadata());
159+
beanRegistry.registerBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, proxyRegistryBeanDef);
160+
return proxyRegistryBeanDef;
161+
}
162+
else {
163+
return (RootBeanDefinition) beanRegistry.getBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME);
164+
}
165+
}
166+
157167
/**
158168
* This method is called before any bean definition registrations are made.
159169
* Subclasses must implement it to register the HTTP Services for which bean
@@ -175,7 +185,7 @@ private ClassPathScanningCandidateComponentProvider getScanner() {
175185
return this.scanner;
176186
}
177187

178-
private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) {
188+
private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) {
179189
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
180190
ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, GroupsMetadata.class);
181191
Assert.state(valueHolder != null, "Expected GroupsMetadata constructor argument at index 0");
@@ -184,12 +194,10 @@ private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) {
184194
target.mergeWith(this.groupsMetadata);
185195
}
186196

187-
private Object getProxyInstance(String registryBeanName, String groupName, String httpServiceType) {
197+
private Object getProxyInstance(String groupName, String httpServiceType) {
188198
Assert.state(this.beanFactory != null, "BeanFactory has not been set");
189-
HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class);
190-
Object proxy = registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType));
191-
Assert.notNull(proxy, "No proxy for HTTP Service [" + httpServiceType + "]");
192-
return proxy;
199+
HttpServiceProxyRegistry registry = this.beanFactory.getBean(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, HttpServiceProxyRegistry.class);
200+
return registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType));
193201
}
194202

195203

Diff for: spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package org.springframework.web.service.registry;
1818

1919
import java.util.Collection;
20+
import java.util.Collections;
2021
import java.util.LinkedHashMap;
2122
import java.util.LinkedHashSet;
2223
import java.util.Map;
2324
import java.util.Set;
2425
import java.util.function.BiConsumer;
2526
import java.util.stream.Collectors;
27+
import java.util.stream.Stream;
2628

2729
import org.springframework.util.Assert;
2830
import org.springframework.util.ClassUtils;
@@ -37,8 +39,16 @@
3739
*/
3840
final class GroupsMetadata {
3941

40-
private final Map<String, DefaultRegistration> groupMap = new LinkedHashMap<>();
42+
private final Map<String, DefaultRegistration> groupMap;
4143

44+
public GroupsMetadata() {
45+
this(Collections.emptyList());
46+
}
47+
48+
GroupsMetadata(Iterable<DefaultRegistration> registrations) {
49+
this.groupMap = new LinkedHashMap<>();
50+
registrations.forEach(registration -> this.groupMap.put(registration.name(), registration));
51+
}
4252

4353
/**
4454
* Create a registration for the given group name, or return an existing
@@ -85,6 +95,13 @@ public static Class<?> loadClass(String type) {
8595
}
8696
}
8797

98+
/**
99+
* Return the raw {@link DefaultRegistration registrations}.
100+
*/
101+
Stream<DefaultRegistration> registrations() {
102+
return this.groupMap.values().stream();
103+
}
104+
88105

89106
/**
90107
* Registration metadata for an {@link HttpServiceGroup}.
@@ -102,17 +119,22 @@ interface Registration {
102119
/**
103120
* Default implementation of {@link Registration}.
104121
*/
105-
private static class DefaultRegistration implements Registration {
122+
static class DefaultRegistration implements Registration {
106123

107124
private final String name;
108125

109126
private HttpServiceGroup.ClientType clientType;
110127

111-
private final Set<String> typeNames = new LinkedHashSet<>();
128+
private final Set<String> typeNames;
112129

113130
DefaultRegistration(String name, HttpServiceGroup.ClientType clientType) {
131+
this(name, clientType, new LinkedHashSet<>());
132+
}
133+
134+
DefaultRegistration(String name, HttpServiceGroup.ClientType clientType, Set<String> typeNames) {
114135
this.name = name;
115136
this.clientType = clientType;
137+
this.typeNames = typeNames;
116138
}
117139

118140
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.registry;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.LinkedHashSet;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
25+
import javax.lang.model.element.Modifier;
26+
27+
import org.jspecify.annotations.Nullable;
28+
29+
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
30+
import org.springframework.aot.generate.ValueCodeGenerator;
31+
import org.springframework.javapoet.CodeBlock;
32+
import org.springframework.web.service.registry.GroupsMetadata.DefaultRegistration;
33+
34+
/**
35+
* {@link ValueCodeGenerator.Delegate} for {@link GroupsMetadata}.
36+
*
37+
* @author Stephane Nicoll
38+
* @since 7.0
39+
*/
40+
final class GroupsMetadataValueDelegate implements ValueCodeGenerator.Delegate {
41+
42+
@Override
43+
public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
44+
if (value instanceof DefaultRegistration registration) {
45+
return generateRegistrationCode(valueCodeGenerator, registration);
46+
}
47+
if (value instanceof GroupsMetadata groupsMetadata) {
48+
return generateGroupsMetadataCode(valueCodeGenerator, groupsMetadata);
49+
}
50+
return null;
51+
}
52+
53+
public CodeBlock generateRegistrationCode(ValueCodeGenerator
54+
valueCodeGenerator, DefaultRegistration value) {
55+
CodeBlock.Builder code = CodeBlock.builder();
56+
code.add("new $T($S, $L, $L)", DefaultRegistration.class, value.name(),
57+
valueCodeGenerator.generateCode(value.clientType()),
58+
!value.httpServiceTypeNames().isEmpty() ?
59+
valueCodeGenerator.generateCode(value.httpServiceTypeNames()) :
60+
CodeBlock.of("new $T()", LinkedHashSet.class));
61+
return code.build();
62+
}
63+
64+
private CodeBlock generateGroupsMetadataCode(ValueCodeGenerator valueCodeGenerator, GroupsMetadata groupsMetadata) {
65+
Collection<DefaultRegistration> registrations = groupsMetadata.registrations()
66+
.collect(Collectors.toCollection(ArrayList::new));
67+
if (valueCodeGenerator.getGeneratedMethods() != null) {
68+
return valueCodeGenerator.getGeneratedMethods().add("getGroupsMetadata", method -> method
69+
.addJavadoc("Create the {@link $T}.", GroupsMetadata.class)
70+
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
71+
.returns(GroupsMetadata.class)
72+
.addCode(generateGroupsMetadataMethod(valueCodeGenerator, registrations))).toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none());
73+
}
74+
else {
75+
return CodeBlock.of("new $T($L)", GroupsMetadata.class, valueCodeGenerator.generateCode(registrations));
76+
}
77+
}
78+
79+
private CodeBlock generateGroupsMetadataMethod(
80+
ValueCodeGenerator valueCodeGenerator, Collection<DefaultRegistration> registrations) {
81+
82+
CodeBlock.Builder code = CodeBlock.builder();
83+
String registrationsVariable = "registrations";
84+
code.addStatement("$T<$T> $L = new $T<>()", List.class, DefaultRegistration.class,
85+
registrationsVariable, ArrayList.class);
86+
registrations.forEach(registration ->
87+
code.addStatement("$L.add($L)", registrationsVariable,
88+
valueCodeGenerator.generateCode(registration))
89+
);
90+
code.addStatement("return new $T($L)", GroupsMetadata.class, registrationsVariable);
91+
return code.build();
92+
}
93+
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.registry;
18+
19+
import javax.lang.model.element.Modifier;
20+
21+
import org.jspecify.annotations.Nullable;
22+
23+
import org.springframework.aot.generate.GeneratedMethod;
24+
import org.springframework.aot.generate.GenerationContext;
25+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
26+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
27+
import org.springframework.beans.factory.aot.BeanRegistrationCode;
28+
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
29+
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
30+
import org.springframework.beans.factory.support.InstanceSupplier;
31+
import org.springframework.beans.factory.support.RegisteredBean;
32+
import org.springframework.javapoet.ClassName;
33+
import org.springframework.javapoet.CodeBlock;
34+
35+
import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE;
36+
import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME;
37+
38+
/**
39+
* {@link BeanRegistrationAotProcessor} for HTTP service proxy support.
40+
*
41+
* @author Stephane Nicoll
42+
* @see AbstractHttpServiceRegistrar
43+
*/
44+
final class HttpServiceProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
45+
46+
@Override
47+
public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
48+
Object value = registeredBean.getMergedBeanDefinition().getAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE);
49+
if (value instanceof String groupName) {
50+
return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments ->
51+
new HttpServiceProxyRegistrationCodeFragments(codeFragments, groupName, registeredBean.getBeanClass()));
52+
}
53+
return null;
54+
}
55+
56+
private static class HttpServiceProxyRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator {
57+
58+
private static final String REGISTERED_BEAN_PARAMETER = "registeredBean";
59+
60+
private final String groupName;
61+
62+
private final Class<?> clientType;
63+
64+
HttpServiceProxyRegistrationCodeFragments(BeanRegistrationCodeFragments delegate,
65+
String groupName, Class<?> clientType) {
66+
super(delegate);
67+
this.groupName = groupName;
68+
this.clientType = clientType;
69+
}
70+
71+
@Override
72+
public ClassName getTarget(RegisteredBean registeredBean) {
73+
return ClassName.get(registeredBean.getBeanClass());
74+
}
75+
76+
@Override
77+
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
78+
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods()
79+
.add("getHttpServiceProxy", method -> {
80+
method.addJavadoc("Create the HTTP service proxy for {@link $T} and group {@code $L}.",
81+
this.clientType, this.groupName);
82+
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
83+
method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER);
84+
method.returns(Object.class);
85+
method.addStatement("return $L.getBeanFactory().getBean($S, $T.class).getClient($S, $T.class)",
86+
REGISTERED_BEAN_PARAMETER, HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME,
87+
HttpServiceProxyRegistry.class, this.groupName, this.clientType);
88+
});
89+
return CodeBlock.of("$T.of($L)", InstanceSupplier.class, generatedMethod.toMethodReference().toCodeBlock());
90+
}
91+
92+
}
93+
94+
}

Diff for: spring-web/src/main/resources/META-INF/spring/aot.factories

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ org.springframework.http.converter.json.ProblemDetailRuntimeHints,\
66
org.springframework.web.util.WebUtilRuntimeHints
77

88
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
9-
org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor
9+
org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor,\
10+
org.springframework.web.service.registry.HttpServiceProxyBeanRegistrationAotProcessor
11+
12+
org.springframework.aot.generate.ValueCodeGenerator$Delegate=\
13+
org.springframework.web.service.registry.GroupsMetadataValueDelegate

0 commit comments

Comments
 (0)