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
This commit is contained in:
Phillip Webb
2025-04-29 19:51:58 -07:00
committed by rstoyanchev
parent 13f9fed4bc
commit abbee1a050
7 changed files with 93 additions and 36 deletions

View File

@@ -23,6 +23,7 @@ import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -30,7 +31,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.OverridingClassLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
import org.springframework.web.service.registry.ImportHttpServices;
@@ -112,6 +115,22 @@ public class RestClientProxyRegistryIntegrationTests {
assertThat(request.getPath()).isEqualTo("/greetingB?input=b");
}
@Test
void beansAreCreatedUsingBeanClassLoader() {
ClassLoader beanClassLoader = new OverridingClassLoader(getClass().getClassLoader()) {
protected boolean isEligibleForOverriding(String className) {
return className.contains("EchoA");
};
};
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.setClassLoader(beanClassLoader);
context.register(ClassUtils.resolveClassName(ListingConfig.class.getName(), beanClassLoader));
context.refresh();
assertThat(context.getBean(ClassUtils.resolveClassName(EchoA.class.getName(), beanClassLoader))
.getClass()
.getClassLoader()).isSameAs(beanClassLoader);
}
private static class ClientConfig {

View File

@@ -92,7 +92,7 @@ class GroupsMetadataValueDelegateTests {
@Test
void generateGroupsMetadataEmpty() {
compile(new GroupsMetadata(), (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()).isEmpty()));
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader())).isEmpty()));
}
@Test
@@ -100,7 +100,7 @@ class GroupsMetadataValueDelegateTests {
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())
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
.singleElement().satisfies(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class))));
}
@@ -115,7 +115,7 @@ class GroupsMetadataValueDelegateTests {
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())
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
.hasSize(2)));
@@ -130,7 +130,7 @@ class GroupsMetadataValueDelegateTests {
.addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName()));
compile(groupsMetadata, (instance, compiled) -> assertThat(instance)
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups())
.isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups(compiled.getClassLoader()))
.satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class))
.satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class))
.hasSize(2)));

View File

@@ -159,7 +159,7 @@ public class HttpServiceRegistrarTests {
GroupsMetadata metadata = (GroupsMetadata) valueHolder.getValue();
assertThat(metadata).isNotNull();
return metadata.groups().stream()
return metadata.groups(null).stream()
.collect(Collectors.toMap(HttpServiceGroup::name, Function.identity()));
}