Skip to content

Commit cd8a3f0

Browse files
committed
Support property placeholders in HTTP service registry
Implement EmbeddedValueResolverAware to resolve ${...} placeholders in @HttpExchange URL attributes. Closes gh-36017 Signed-off-by: Juhwan Lee <[email protected]>
1 parent d3a385d commit cd8a3f0

File tree

2 files changed

+111
-4
lines changed

2 files changed

+111
-4
lines changed

spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
import org.springframework.beans.factory.InitializingBean;
4040
import org.springframework.context.ApplicationContext;
4141
import org.springframework.context.ApplicationContextAware;
42+
import org.springframework.context.EmbeddedValueResolverAware;
4243
import org.springframework.util.Assert;
4344
import org.springframework.util.ClassUtils;
4445
import org.springframework.util.LinkedMultiValueMap;
4546
import org.springframework.util.MultiValueMap;
47+
import org.springframework.util.StringValueResolver;
4648
import org.springframework.web.service.invoker.HttpExchangeAdapter;
4749
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
4850

@@ -61,8 +63,8 @@
6163
* @see AbstractHttpServiceRegistrar
6264
*/
6365
public final class HttpServiceProxyRegistryFactoryBean
64-
implements ApplicationContextAware, BeanClassLoaderAware, InitializingBean,
65-
FactoryBean<HttpServiceProxyRegistry> {
66+
implements ApplicationContextAware, BeanClassLoaderAware, EmbeddedValueResolverAware,
67+
InitializingBean, FactoryBean<HttpServiceProxyRegistry> {
6668

6769
private static final Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> groupAdapters =
6870
GroupAdapterInitializer.initGroupAdapters();
@@ -76,6 +78,7 @@ public final class HttpServiceProxyRegistryFactoryBean
7678

7779
private @Nullable HttpServiceProxyRegistry proxyRegistry;
7880

81+
private @Nullable StringValueResolver embeddedValueResolver;
7982

8083
HttpServiceProxyRegistryFactoryBean(GroupsMetadata groupsMetadata) {
8184
this.groupsMetadata = groupsMetadata;
@@ -92,6 +95,11 @@ public void setBeanClassLoader(ClassLoader beanClassLoader) {
9295
this.beanClassLoader = beanClassLoader;
9396
}
9497

98+
@Override
99+
public void setEmbeddedValueResolver(StringValueResolver resolver) {
100+
this.embeddedValueResolver = resolver;
101+
}
102+
95103
@Override
96104
public Class<?> getObjectType() {
97105
return HttpServiceProxyRegistry.class;
@@ -105,7 +113,7 @@ public void afterPropertiesSet() {
105113

106114
// Create the groups from the metadata
107115
Set<ConfigurableGroup> groups = this.groupsMetadata.groups(this.beanClassLoader).stream()
108-
.map(ConfigurableGroup::new)
116+
.map(group -> new ConfigurableGroup(group, this.embeddedValueResolver))
109117
.collect(Collectors.toSet());
110118

111119
// Apply group configurers
@@ -169,11 +177,14 @@ private static final class ConfigurableGroup {
169177

170178
private @Nullable Object clientBuilder;
171179

180+
private final @Nullable StringValueResolver embeddedValueResolver;
181+
172182
private final HttpServiceProxyFactory.Builder proxyFactoryBuilder = HttpServiceProxyFactory.builder();
173183

174-
ConfigurableGroup(HttpServiceGroup group) {
184+
ConfigurableGroup(HttpServiceGroup group, @Nullable StringValueResolver embeddedValueResolver) {
175185
this.group = group;
176186
this.groupAdapter = getGroupAdapter(group.clientType());
187+
this.embeddedValueResolver = embeddedValueResolver;
177188
}
178189

179190
private static HttpServiceGroupAdapter<?> getGroupAdapter(HttpServiceGroup.ClientType clientType) {
@@ -218,6 +229,9 @@ private <CB> CB getClientBuilder() {
218229
public Map<Class<?>, Object> createProxies() {
219230
Map<Class<?>, Object> map = new LinkedHashMap<>(this.group.httpServiceTypes().size());
220231
HttpExchangeAdapter adapter = this.groupAdapter.createExchangeAdapter(getClientBuilder());
232+
if (this.embeddedValueResolver != null) {
233+
this.proxyFactoryBuilder.embeddedValueResolver(this.embeddedValueResolver);
234+
}
221235
HttpServiceProxyFactory factory = this.proxyFactoryBuilder.exchangeAdapter(adapter).build();
222236
this.group.httpServiceTypes().forEach(type -> map.put(type, factory.createClient(type)));
223237
return map;

spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBeanTests.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@
1616

1717
package org.springframework.web.service.registry;
1818

19+
import java.net.URI;
1920
import java.util.List;
2021
import java.util.function.Predicate;
2122

2223
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
2325
import org.mockito.Mockito;
2426

2527
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2628
import org.springframework.http.client.ClientHttpRequestFactory;
2729
import org.springframework.util.LinkedMultiValueMap;
2830
import org.springframework.util.MultiValueMap;
31+
import org.springframework.util.StringValueResolver;
2932
import org.springframework.web.client.RestClient;
3033
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
34+
import org.springframework.web.service.annotation.GetExchange;
35+
import org.springframework.web.service.annotation.HttpExchange;
3136
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
3237
import org.springframework.web.service.registry.echo.EchoA;
3338
import org.springframework.web.service.registry.echo.EchoB;
@@ -37,6 +42,7 @@
3742
import org.springframework.web.testfixture.http.client.MockClientHttpResponse;
3843

3944
import static org.assertj.core.api.Assertions.assertThat;
45+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4046
import static org.mockito.ArgumentMatchers.any;
4147
import static org.mockito.BDDMockito.given;
4248
import static org.mockito.Mockito.atLeastOnce;
@@ -95,6 +101,87 @@ void initializeClientBuilder() throws Exception {
95101
verify(requestFactory, atLeastOnce()).createRequest(any(), any());
96102
}
97103

104+
@Test
105+
void propertyPlaceholderInHttpExchangeUrlIsResolved() throws Exception {
106+
GroupsMetadata groupsMetadata = new GroupsMetadata();
107+
groupsMetadata.getOrCreateGroup("testGroup", REST_CLIENT)
108+
.httpServiceTypeNames()
109+
.add(PlaceholderService.class.getName());
110+
111+
ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class);
112+
MockClientHttpRequest mockRequest = new MockClientHttpRequest();
113+
mockRequest.setResponse(new MockClientHttpResponse());
114+
115+
ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
116+
given(requestFactory.createRequest(uriCaptor.capture(), any())).willReturn(mockRequest);
117+
118+
StringValueResolver resolver = value -> {
119+
if (value.contains("${test.base.url}")) {
120+
return value.replace("${test.base.url}", "https://api.example.com");
121+
}
122+
return value;
123+
};
124+
125+
RestClient.Builder clientBuilder = RestClient.builder().requestFactory(requestFactory);
126+
127+
RestClientHttpServiceGroupConfigurer configurer = groups -> groups.forEachClient(group -> clientBuilder);
128+
129+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
130+
context.registerBean(RestClientHttpServiceGroupConfigurer.class, () -> configurer);
131+
context.refresh();
132+
133+
HttpServiceProxyRegistryFactoryBean factoryBean = new HttpServiceProxyRegistryFactoryBean(groupsMetadata);
134+
135+
factoryBean.setApplicationContext(context);
136+
factoryBean.setBeanClassLoader(getClass().getClassLoader());
137+
factoryBean.setEmbeddedValueResolver(resolver);
138+
factoryBean.afterPropertiesSet();
139+
140+
HttpServiceProxyRegistry registry = factoryBean.getObject();
141+
PlaceholderService service = registry.getClient(PlaceholderService.class);
142+
service.callEndpoint();
143+
144+
URI requestedUri = uriCaptor.getValue();
145+
146+
assertThat(requestedUri.toString())
147+
.startsWith("https://api.example.com")
148+
.doesNotContain("${")
149+
.contains("/endpoint");
150+
}
151+
152+
@Test
153+
void withoutResolverPlaceholderRemainsUnresolved() throws Exception {
154+
GroupsMetadata groupsMetadata = new GroupsMetadata();
155+
groupsMetadata.getOrCreateGroup("testGroup", REST_CLIENT)
156+
.httpServiceTypeNames()
157+
.add(PlaceholderService.class.getName());
158+
159+
ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class);
160+
MockClientHttpRequest capturedRequest = new MockClientHttpRequest();
161+
capturedRequest.setResponse(new MockClientHttpResponse());
162+
given(requestFactory.createRequest(any(), any())).willReturn(capturedRequest);
163+
164+
RestClient.Builder clientBuilder = RestClient.builder().requestFactory(requestFactory);
165+
RestClientHttpServiceGroupConfigurer configurer = groups ->
166+
groups.forEachClient(group -> clientBuilder);
167+
168+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
169+
context.registerBean(RestClientHttpServiceGroupConfigurer.class, () -> configurer);
170+
context.refresh();
171+
172+
HttpServiceProxyRegistryFactoryBean factoryBean = new HttpServiceProxyRegistryFactoryBean(groupsMetadata);
173+
factoryBean.setApplicationContext(context);
174+
factoryBean.setBeanClassLoader(getClass().getClassLoader());
175+
factoryBean.afterPropertiesSet();
176+
177+
HttpServiceProxyRegistry registry = factoryBean.getObject();
178+
PlaceholderService service = registry.getClient(PlaceholderService.class);
179+
180+
assertThatThrownBy(service::callEndpoint)
181+
.isInstanceOf(IllegalArgumentException.class)
182+
.hasMessageContaining("test.base.url");
183+
}
184+
98185
private HttpServiceProxyRegistry initProxyRegistry(
99186
RestClientHttpServiceGroupConfigurer groupConfigurer, GroupsMetadata groupsMetadata) {
100187

@@ -136,4 +223,10 @@ public void withGroup(HttpServiceGroup group, RestClient clientBuilder, HttpServ
136223
}
137224
}
138225

226+
@HttpExchange(url = "${test.base.url}")
227+
interface PlaceholderService {
228+
229+
@GetExchange("/endpoint")
230+
String callEndpoint();
231+
}
139232
}

0 commit comments

Comments
 (0)