Skip to content

Commit abbee1a

Browse files
philwebbrstoyanchev
authored andcommitted
Use bean class loader when creating interface clients
Update interface client code to replace `GroupsMetadata.loadClass` calls with `ClassUtils.resolveClassName` passing in the bean class loader. Since the bean class loader in injected after construction, some minor refactoring has been applied to `HttpServiceProxyRegistryFactoryBean`. The class now stores `GroupsMetadata` and only loads the types in in `afterPropertiesSet`. The `HttpServiceProxyFactory` class has also been updated to ensure that the proxy is created using the class loader of the service type, rather than the thread context class loader. Fixes gh-34846
1 parent 13f9fed commit abbee1a

File tree

7 files changed

+93
-36
lines changed

7 files changed

+93
-36
lines changed

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,14 @@ public <S> S createClient(Class<S> serviceType) {
8989
.map(method -> createHttpServiceMethod(serviceType, method))
9090
.toList();
9191

92-
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(httpServiceMethods));
92+
return getProxy(serviceType, httpServiceMethods);
93+
}
94+
95+
@SuppressWarnings("unchecked")
96+
private <S> S getProxy(Class<S> serviceType, List<HttpServiceMethod> httpServiceMethods) {
97+
MethodInterceptor interceptor = new HttpServiceMethodInterceptor(httpServiceMethods);
98+
ProxyFactory proxyFactory = new ProxyFactory(serviceType, interceptor);
99+
return (S) proxyFactory.getProxy(serviceType.getClassLoader());
93100
}
94101

95102
private boolean isExchangeMethod(Method method) {

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.jspecify.annotations.Nullable;
2020

2121
import org.springframework.beans.BeansException;
22+
import org.springframework.beans.factory.BeanClassLoaderAware;
2223
import org.springframework.beans.factory.BeanFactory;
2324
import org.springframework.beans.factory.BeanFactoryAware;
2425
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
@@ -38,6 +39,7 @@
3839
import org.springframework.core.type.classreading.MetadataReader;
3940
import org.springframework.core.type.filter.AnnotationTypeFilter;
4041
import org.springframework.util.Assert;
42+
import org.springframework.util.ClassUtils;
4143
import org.springframework.web.service.annotation.HttpExchange;
4244

4345
/**
@@ -74,7 +76,7 @@
7476
* @see HttpServiceProxyRegistryFactoryBean
7577
*/
7678
public abstract class AbstractHttpServiceRegistrar implements
77-
ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware {
79+
ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware, BeanClassLoaderAware {
7880

7981
/**
8082
* The bean name of the {@link HttpServiceProxyRegistry}.
@@ -91,6 +93,8 @@ public abstract class AbstractHttpServiceRegistrar implements
9193

9294
private @Nullable BeanFactory beanFactory;
9395

96+
private @Nullable ClassLoader beanClassLoader;
97+
9498
private final GroupsMetadata groupsMetadata = new GroupsMetadata();
9599

96100
private @Nullable ClassPathScanningCandidateComponentProvider scanner;
@@ -121,6 +125,11 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
121125
this.beanFactory = beanFactory;
122126
}
123127

128+
@Override
129+
public void setBeanClassLoader(ClassLoader beanClassLoader) {
130+
this.beanClassLoader = beanClassLoader;
131+
}
132+
124133

125134
@Override
126135
public final void registerBeanDefinitions(
@@ -197,7 +206,7 @@ private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) {
197206
private Object getProxyInstance(String groupName, String httpServiceType) {
198207
Assert.state(this.beanFactory != null, "BeanFactory has not been set");
199208
HttpServiceProxyRegistry registry = this.beanFactory.getBean(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, HttpServiceProxyRegistry.class);
200-
return registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType));
209+
return registry.getClient(groupName, ClassUtils.resolveClassName(httpServiceType, this.beanClassLoader));
201210
}
202211

203212

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

+11-13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.stream.Collectors;
2727
import java.util.stream.Stream;
2828

29+
import org.jspecify.annotations.Nullable;
30+
2931
import org.springframework.util.Assert;
3032
import org.springframework.util.ClassUtils;
3133

@@ -82,17 +84,11 @@ public void forEachRegistration(BiConsumer<String, Set<String>> consumer) {
8284
/**
8385
* Create the {@link HttpServiceGroup}s for all registrations.
8486
*/
85-
public Collection<HttpServiceGroup> groups() {
86-
return this.groupMap.values().stream().map(DefaultRegistration::toHttpServiceGroup).toList();
87-
}
88-
89-
public static Class<?> loadClass(String type) {
90-
try {
91-
return ClassUtils.forName(type, GroupsMetadata.class.getClassLoader());
92-
}
93-
catch (ClassNotFoundException ex) {
94-
throw new IllegalStateException("Failed to load '" + type + "'", ex);
95-
}
87+
public Collection<HttpServiceGroup> groups(@Nullable ClassLoader classLoader) {
88+
return this.groupMap.values()
89+
.stream()
90+
.map(registration -> registration.toHttpServiceGroup(classLoader))
91+
.toList();
9692
}
9793

9894
/**
@@ -169,10 +165,12 @@ public DefaultRegistration clientType(HttpServiceGroup.ClientType other) {
169165
/**
170166
* Create the {@link HttpServiceGroup} from the metadata.
171167
*/
172-
public HttpServiceGroup toHttpServiceGroup() {
168+
public HttpServiceGroup toHttpServiceGroup(@Nullable ClassLoader classLoader) {
173169
return new RegisteredGroup(this.name,
174170
(this.clientType.isUnspecified() ? HttpServiceGroup.ClientType.REST_CLIENT : this.clientType),
175-
this.typeNames.stream().map(GroupsMetadata::loadClass).collect(Collectors.toSet()));
171+
this.typeNames.stream()
172+
.map(typeName -> ClassUtils.resolveClassName(typeName, classLoader))
173+
.collect(Collectors.toSet()));
176174
}
177175

178176
@Override

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

+39-15
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import org.springframework.beans.BeanUtils;
3333
import org.springframework.beans.BeansException;
34+
import org.springframework.beans.factory.BeanClassLoaderAware;
3435
import org.springframework.beans.factory.FactoryBean;
3536
import org.springframework.beans.factory.InitializingBean;
3637
import org.springframework.context.ApplicationContext;
@@ -57,27 +58,24 @@
5758
* @see AbstractHttpServiceRegistrar
5859
*/
5960
public final class HttpServiceProxyRegistryFactoryBean
60-
implements ApplicationContextAware, InitializingBean, FactoryBean<HttpServiceProxyRegistry> {
61+
implements ApplicationContextAware, BeanClassLoaderAware, InitializingBean,
62+
FactoryBean<HttpServiceProxyRegistry> {
6163

6264
private static final Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> groupAdapters =
6365
GroupAdapterInitializer.initGroupAdapters();
6466

6567

66-
private final Set<ProxyHttpServiceGroup> groupSet;
68+
private final GroupsMetadata groupsMetadata;
6769

6870
private @Nullable ApplicationContext applicationContext;
6971

72+
private @Nullable ClassLoader beanClassLoader;
73+
7074
private @Nullable HttpServiceProxyRegistry proxyRegistry;
7175

7276

7377
HttpServiceProxyRegistryFactoryBean(GroupsMetadata groupsMetadata) {
74-
this.groupSet = groupsMetadata.groups().stream()
75-
.map(group -> {
76-
HttpServiceGroupAdapter<?> adapter = groupAdapters.get(group.clientType());
77-
Assert.state(adapter != null, "No HttpServiceGroupAdapter for type " + group.clientType());
78-
return new ProxyHttpServiceGroup(group, adapter);
79-
})
80-
.collect(Collectors.toSet());
78+
this.groupsMetadata = groupsMetadata;
8179
}
8280

8381

@@ -86,6 +84,11 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
8684
this.applicationContext = applicationContext;
8785
}
8886

87+
@Override
88+
public void setBeanClassLoader(ClassLoader beanClassLoader) {
89+
this.beanClassLoader = beanClassLoader;
90+
}
91+
8992
@Override
9093
public Class<?> getObjectType() {
9194
return HttpServiceProxyRegistry.class;
@@ -95,18 +98,25 @@ public Class<?> getObjectType() {
9598
@Override
9699
public void afterPropertiesSet() {
97100
Assert.notNull(this.applicationContext, "ApplicationContext not initialized");
101+
Assert.notNull(this.beanClassLoader, "BeanClassLoader not initialized");
102+
103+
// Create the groups from the metadata
104+
Set<ProxyHttpServiceGroup> groups = this.groupsMetadata.groups(this.beanClassLoader)
105+
.stream()
106+
.map(ProxyHttpServiceGroup::new)
107+
.collect(Collectors.toSet());
98108

99109
// Apply group configurers
100110
groupAdapters.forEach((clientType, groupAdapter) ->
101111
this.applicationContext.getBeanProvider(groupAdapter.getConfigurerType())
102112
.orderedStream()
103-
.forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType))));
113+
.forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(groups, clientType))));
104114

105115
// Create proxies
106-
Map<String, Map<Class<?>, Object>> groupProxyMap = this.groupSet.stream()
116+
Map<String, Map<Class<?>, Object>> proxies = groups.stream()
107117
.collect(Collectors.toMap(ProxyHttpServiceGroup::name, ProxyHttpServiceGroup::createProxies));
108118

109-
this.proxyRegistry = new DefaultHttpServiceProxyRegistry(groupProxyMap);
119+
this.proxyRegistry = new DefaultHttpServiceProxyRegistry(proxies);
110120
}
111121

112122

@@ -159,12 +169,17 @@ private static final class ProxyHttpServiceGroup implements HttpServiceGroup {
159169

160170
private BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer = (group, builder) -> {};
161171

172+
ProxyHttpServiceGroup(HttpServiceGroup group) {
173+
this(group, getHttpServiceGroupAdapter(group.clientType()));
174+
}
175+
162176
ProxyHttpServiceGroup(HttpServiceGroup group, HttpServiceGroupAdapter<?> groupAdapter) {
163177
this.declaredGroup = group;
164178
this.groupAdapter = groupAdapter;
165179
this.clientBuilder = groupAdapter.createClientBuilder();
166180
}
167181

182+
168183
@Override
169184
public String name() {
170185
return this.declaredGroup.name();
@@ -208,17 +223,26 @@ private <CB> HttpExchangeAdapter initExchangeAdapter() {
208223
public String toString() {
209224
return getClass().getSimpleName() + "[id=" + name() + "]";
210225
}
226+
227+
private static HttpServiceGroupAdapter<?> getHttpServiceGroupAdapter(HttpServiceGroup.ClientType clientType) {
228+
HttpServiceGroupAdapter<?> adapter = groupAdapters.get(clientType);
229+
Assert.state(adapter != null, "No HttpServiceGroupAdapter for type " + clientType);
230+
return adapter;
231+
}
211232
}
212233

213234

214235
/**
215236
* Default implementation of Groups that helps to configure the set of declared groups.
216237
*/
217-
private final class DefaultGroups<CB> implements HttpServiceGroupConfigurer.Groups<CB> {
238+
private static final class DefaultGroups<CB> implements HttpServiceGroupConfigurer.Groups<CB> {
239+
240+
private final Set<ProxyHttpServiceGroup> groups;
218241

219242
private Predicate<HttpServiceGroup> filter;
220243

221-
DefaultGroups(HttpServiceGroup.ClientType clientType) {
244+
DefaultGroups(Set<ProxyHttpServiceGroup> groups, HttpServiceGroup.ClientType clientType) {
245+
this.groups = groups;
222246
this.filter = group -> group.clientType().equals(clientType);
223247
}
224248

@@ -255,7 +279,7 @@ public void configure(
255279
BiConsumer<HttpServiceGroup, CB> clientConfigurer,
256280
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
257281

258-
groupSet.stream().filter(this.filter).forEach(group ->
282+
this.groups.stream().filter(this.filter).forEach(group ->
259283
group.apply(clientConfigurer, proxyFactoryConfigurer));
260284
}
261285
}

spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
import okhttp3.mockwebserver.RecordedRequest;
2424
import org.junit.jupiter.api.AfterEach;
2525
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
2627
import org.junit.jupiter.params.ParameterizedTest;
2728
import org.junit.jupiter.params.provider.ValueSource;
2829

2930
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3031
import org.springframework.context.annotation.Bean;
3132
import org.springframework.context.annotation.Configuration;
3233
import org.springframework.context.annotation.Import;
34+
import org.springframework.core.OverridingClassLoader;
3335
import org.springframework.core.type.AnnotationMetadata;
36+
import org.springframework.util.ClassUtils;
3437
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
3538
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
3639
import org.springframework.web.service.registry.ImportHttpServices;
@@ -112,6 +115,22 @@ void basic(Class<?> configClass) throws InterruptedException {
112115
assertThat(request.getPath()).isEqualTo("/greetingB?input=b");
113116
}
114117

118+
@Test
119+
void beansAreCreatedUsingBeanClassLoader() {
120+
ClassLoader beanClassLoader = new OverridingClassLoader(getClass().getClassLoader()) {
121+
122+
protected boolean isEligibleForOverriding(String className) {
123+
return className.contains("EchoA");
124+
};
125+
};
126+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
127+
context.setClassLoader(beanClassLoader);
128+
context.register(ClassUtils.resolveClassName(ListingConfig.class.getName(), beanClassLoader));
129+
context.refresh();
130+
assertThat(context.getBean(ClassUtils.resolveClassName(EchoA.class.getName(), beanClassLoader))
131+
.getClass()
132+
.getClassLoader()).isSameAs(beanClassLoader);
133+
}
115134

116135
private static class ClientConfig {
117136

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ void generateRegistrationWitHttpServiceTypeNames() {
9292
@Test
9393
void generateGroupsMetadataEmpty() {
9494
compile(new GroupsMetadata(), (instance, compiled) -> assertThat(instance)
95-
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()).isEmpty()));
95+
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader())).isEmpty()));
9696
}
9797

9898
@Test
9999
void generateGroupsMetadataSingleGroup() {
100100
GroupsMetadata groupsMetadata = new GroupsMetadata();
101101
groupsMetadata.getOrCreateGroup("test-group", ClientType.REST_CLIENT).httpServiceTypeNames().add(EchoA.class.getName());
102102
compile(groupsMetadata, (instance, compiled) -> assertThat(instance)
103-
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
103+
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
104104
.singleElement().satisfies(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class))));
105105
}
106106

@@ -115,7 +115,7 @@ void generateGroupsMetadataMultipleGroupsSimple() {
115115
Function<GeneratedClass, ValueCodeGenerator> valueCodeGeneratorFactory = generatedClass ->
116116
ValueCodeGenerator.withDefaults().add(List.of(new GroupsMetadataValueDelegate()));
117117
compile(valueCodeGeneratorFactory, groupsMetadata, (instance, compiled) -> assertThat(instance)
118-
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
118+
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
119119
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
120120
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
121121
.hasSize(2)));
@@ -130,7 +130,7 @@ void generateGroupsMetadataMultipleGroups() {
130130
.addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName()));
131131

132132
compile(groupsMetadata, (instance, compiled) -> assertThat(instance)
133-
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
133+
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
134134
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
135135
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
136136
.hasSize(2)));

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private Map<String, HttpServiceGroup> groupMap() {
159159
GroupsMetadata metadata = (GroupsMetadata) valueHolder.getValue();
160160
assertThat(metadata).isNotNull();
161161

162-
return metadata.groups().stream()
162+
return metadata.groups(null).stream()
163163
.collect(Collectors.toMap(HttpServiceGroup::name, Function.identity()));
164164
}
165165

0 commit comments

Comments
 (0)