Skip to content

Commit 1e71426

Browse files
committed
Add ImportHttpServices.GroupProvider support
Update `@ImportHttpServices` to allow the group to either be specified using the `group` attribute or a `groupProvider` implementation. Using a `groupProvider` allows the group name to be deduced from the class metadata. For example, users could use a custom annotation to hold the group name. Groups must either be specified using the `group` attribute *or* the `groupProvider` attribute. Attempting to set both will cause an `IllegalStateException` to be thrown. See gh-35447
1 parent 14096f8 commit 1e71426

File tree

3 files changed

+185
-7
lines changed

3 files changed

+185
-7
lines changed

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

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,26 @@
1616

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

19+
import java.util.Arrays;
20+
import java.util.function.Consumer;
21+
22+
import org.jspecify.annotations.Nullable;
23+
24+
import org.springframework.beans.BeanUtils;
25+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
26+
import org.springframework.beans.factory.config.BeanDefinition;
1927
import org.springframework.core.annotation.MergedAnnotation;
28+
import org.springframework.core.io.ResourceLoader;
2029
import org.springframework.core.type.AnnotationMetadata;
30+
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
31+
import org.springframework.core.type.classreading.MetadataReader;
32+
import org.springframework.core.type.classreading.MetadataReaderFactory;
33+
import org.springframework.util.Assert;
2134
import org.springframework.util.ClassUtils;
2235
import org.springframework.util.ObjectUtils;
36+
import org.springframework.util.StringUtils;
37+
import org.springframework.util.function.ThrowingFunction;
38+
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
2339

2440
/**
2541
* Built-in implementation of {@link AbstractHttpServiceRegistrar} that uses
@@ -33,6 +49,14 @@
3349
*/
3450
class ImportHttpServiceRegistrar extends AbstractHttpServiceRegistrar {
3551

52+
private @Nullable MetadataReaderFactory metadataReaderFactory;
53+
54+
@Override
55+
public void setResourceLoader(ResourceLoader resourceLoader) {
56+
super.setResourceLoader(resourceLoader);
57+
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
58+
}
59+
3660
@Override
3761
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
3862

@@ -50,7 +74,7 @@ protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata m
5074
private void processImportAnnotation(MergedAnnotation<?> annotation, GroupRegistry groupRegistry,
5175
AnnotationMetadata metadata) {
5276

53-
String groupName = annotation.getString("group");
77+
ImportHttpServices.GroupProvider groupProvider = getGroupProvider(annotation);
5478
HttpServiceGroup.ClientType clientType = annotation.getEnum("clientType", HttpServiceGroup.ClientType.class);
5579
Class<?>[] types = annotation.getClassArray("types");
5680
Class<?>[] basePackageClasses = annotation.getClassArray("basePackageClasses");
@@ -60,10 +84,70 @@ private void processImportAnnotation(MergedAnnotation<?> annotation, GroupRegist
6084
basePackages = new String[] { ClassUtils.getPackageName(metadata.getClassName()) };
6185
}
6286

63-
groupRegistry.forGroup(groupName, clientType)
64-
.register(types)
65-
.detectInBasePackages(basePackageClasses)
66-
.detectInBasePackages(basePackages);
87+
registerHttpServices(groupRegistry, groupProvider, clientType, types, basePackageClasses, basePackages);
88+
}
89+
90+
private ImportHttpServices.GroupProvider getGroupProvider(MergedAnnotation<?> annotation) {
91+
String group = annotation.getString("group");
92+
Class<?> groupProvider = annotation.getClass("groupProvider");
93+
if (groupProvider == ImportHttpServices.GroupProvider.class) {
94+
return new FixedGroupProvider(StringUtils.hasText(group) ? group : HttpServiceGroup.DEFAULT_GROUP_NAME);
95+
}
96+
Assert.state(!StringUtils.hasText(group), "'group' cannot be mixed with 'groupProvider'");
97+
return (ImportHttpServices.GroupProvider) BeanUtils.instantiateClass(groupProvider);
98+
}
99+
100+
private void registerHttpServices(GroupRegistry groupRegistry,
101+
ImportHttpServices.GroupProvider groupProvider, ClientType clientType, Class<?>[] types,
102+
Class<?>[] basePackageClasses, String[] basePackages) {
103+
104+
if (groupProvider instanceof FixedGroupProvider fixedGroupProvider) {
105+
String groupName = fixedGroupProvider.group();
106+
groupRegistry.forGroup(groupName, clientType)
107+
.register(types)
108+
.detectInBasePackages(basePackageClasses)
109+
.detectInBasePackages(basePackages);
110+
}
111+
else {
112+
MetadataReaderFactory metadataReaderFactory = (this.metadataReaderFactory != null) ?
113+
this.metadataReaderFactory : new CachingMetadataReaderFactory();
114+
115+
Consumer<AnnotationMetadata> register = metadata -> {
116+
String group = groupProvider.group(metadata);
117+
if (group != null) {
118+
groupRegistry.forGroup(group, clientType).registerTypeNames(metadata.getClassName());
119+
}
120+
};
121+
122+
Arrays.stream(types)
123+
.map(Class::getName)
124+
.map(ThrowingFunction.of(metadataReaderFactory::getMetadataReader))
125+
.map(MetadataReader::getAnnotationMetadata)
126+
.forEach(register);
127+
Arrays.stream(basePackageClasses)
128+
.map(Class::getPackageName)
129+
.flatMap(this::findHttpServices)
130+
.map(this::getMetadata)
131+
.forEach(register);
132+
Arrays.stream(basePackages)
133+
.flatMap(this::findHttpServices)
134+
.map(this::getMetadata)
135+
.forEach(register);
136+
}
137+
}
138+
139+
private AnnotationMetadata getMetadata(BeanDefinition beanDefinition) {
140+
Assert.state(beanDefinition instanceof AnnotatedBeanDefinition,
141+
"AnnotatedBeanDefinition required when using 'groupProvider'");
142+
return ((AnnotatedBeanDefinition) beanDefinition).getMetadata();
143+
}
144+
145+
private static record FixedGroupProvider(String group) implements ImportHttpServices.GroupProvider {
146+
147+
@Override
148+
public String group(AnnotationMetadata metadata) {
149+
return this.group;
150+
}
67151
}
68152

69153
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import java.lang.annotation.RetentionPolicy;
2424
import java.lang.annotation.Target;
2525

26+
import org.jspecify.annotations.Nullable;
27+
2628
import org.springframework.context.annotation.Import;
2729
import org.springframework.core.annotation.AliasFor;
30+
import org.springframework.core.type.AnnotationMetadata;
2831
import org.springframework.web.service.annotation.HttpExchange;
2932

3033
/**
@@ -47,6 +50,7 @@
4750
*
4851
* @author Olga Maciaszek-Sharma
4952
* @author Rossen Stoyanchev
53+
* @author Phillip Webb
5054
* @since 7.0
5155
* @see Container
5256
* @see AbstractHttpServiceRegistrar
@@ -74,8 +78,17 @@
7478
* The name of the HTTP Service group.
7579
* <p>If not specified, declared HTTP Services are grouped under the
7680
* {@link HttpServiceGroup#DEFAULT_GROUP_NAME}.
81+
* @see #groupProvider()
82+
*/
83+
String group() default "";
84+
85+
/**
86+
* Strategy used to provide the name of the HTTP Service group.
87+
* <p>May be used as an alternative to {@link #group()} when the group name can
88+
* be determined from the type.
89+
* @see #group()
7790
*/
78-
String group() default HttpServiceGroup.DEFAULT_GROUP_NAME;
91+
Class<? extends GroupProvider> groupProvider() default GroupProvider.class;
7992

8093
/**
8194
* Detect HTTP Services in the packages of the specified classes, looking
@@ -112,4 +125,19 @@
112125
ImportHttpServices[] value();
113126
}
114127

128+
/**
129+
* Strategy interface to provide the group name for HTTP Service interface.
130+
*/
131+
@FunctionalInterface
132+
interface GroupProvider {
133+
134+
/**
135+
* Provide the group name that should be used for the given HTTP Service interface
136+
* metadata.
137+
* @param metadata the HTTP Service interface metadata
138+
* @return the provided group name or {@code null} if the HTTP Service client
139+
* should not be registered
140+
*/
141+
@Nullable String group(AnnotationMetadata metadata);
142+
}
115143
}

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

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,38 @@
1919
import java.util.Map;
2020
import java.util.function.BiConsumer;
2121

22+
import org.jspecify.annotations.Nullable;
2223
import org.junit.jupiter.api.Test;
2324

2425
import org.springframework.aot.test.generate.TestGenerationContext;
2526
import org.springframework.context.ApplicationContextInitializer;
2627
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2728
import org.springframework.context.aot.ApplicationContextAotGenerator;
2829
import org.springframework.context.support.GenericApplicationContext;
30+
import org.springframework.core.env.StandardEnvironment;
31+
import org.springframework.core.io.DefaultResourceLoader;
2932
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
3033
import org.springframework.core.test.tools.Compiled;
3134
import org.springframework.core.test.tools.TestCompiler;
3235
import org.springframework.core.type.AnnotationMetadata;
36+
import org.springframework.util.ClassUtils;
3337
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
38+
import org.springframework.web.service.registry.ImportHttpServices.GroupProvider;
3439
import org.springframework.web.service.registry.echo.EchoA;
3540
import org.springframework.web.service.registry.echo.EchoB;
3641
import org.springframework.web.service.registry.echo.ScanConventionConfig;
3742
import org.springframework.web.service.registry.greeting.GreetingA;
3843
import org.springframework.web.service.registry.greeting.GreetingB;
3944

4045
import static org.assertj.core.api.Assertions.assertThat;
46+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4147

4248
/**
4349
* Tests for {@link ImportHttpServiceRegistrar}.
4450
*
4551
* @author Rossen Stoyanchev
4652
* @author Stephane Nicoll
53+
* @author Phillip Webb
4754
*/
4855
public class ImportHttpServiceRegistrarTests {
4956

@@ -54,7 +61,14 @@ public class ImportHttpServiceRegistrarTests {
5461

5562
private final TestGroupRegistry groupRegistry = new TestGroupRegistry();
5663

57-
private final ImportHttpServiceRegistrar registrar = new ImportHttpServiceRegistrar();
64+
private final ImportHttpServiceRegistrar registrar;
65+
66+
67+
ImportHttpServiceRegistrarTests() {
68+
this.registrar = new ImportHttpServiceRegistrar();
69+
this.registrar.setEnvironment(new StandardEnvironment());
70+
this.registrar.setResourceLoader(new DefaultResourceLoader());
71+
}
5872

5973

6074
@Test
@@ -112,6 +126,28 @@ void clientType() {
112126
TestGroup.ofListing(GREETING_GROUP, ClientType.WEB_CLIENT, GreetingA.class));
113127
}
114128

129+
@Test
130+
void providedGroupName() {
131+
doRegister(ProvidedGroupNameConfig.class);
132+
assertGroups(
133+
TestGroup.ofListing(ECHO_GROUP, EchoA.class, EchoB.class),
134+
TestGroup.ofListing(GREETING_GROUP, GreetingA.class, GreetingB.class));
135+
}
136+
137+
@Test
138+
void providedGroupNameWhenNull() {
139+
doRegister(ProvidedGroupNameConfigWhenNull.class);
140+
assertGroups(
141+
TestGroup.ofListing(ECHO_GROUP, EchoB.class),
142+
TestGroup.ofListing(GREETING_GROUP, GreetingB.class));
143+
}
144+
145+
@Test
146+
void providedGroupNameWhenGroupAndGroupProviderThrowsException() {
147+
assertThatIllegalStateException().isThrownBy(() -> doRegister(InvalidProvidedGroupNameConfig.class))
148+
.withMessage("'group' cannot be mixed with 'groupProvider'");
149+
}
150+
115151
private void doRegister(Class<?> configClass) {
116152
AnnotationMetadata metadata = AnnotationMetadata.introspect(configClass);
117153
this.registrar.registerHttpServices(this.groupRegistry, metadata);
@@ -162,4 +198,34 @@ static class ScanConfig {
162198
@ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = { GreetingA.class })
163199
static class ClientTypeConfig {
164200
}
201+
202+
@ImportHttpServices(groupProvider = FromClassGroupProvider.class)
203+
static class ProvidedGroupNameConfig {
204+
}
205+
206+
@ImportHttpServices(groupProvider = FromClassGroupProviderWithoutAs.class)
207+
static class ProvidedGroupNameConfigWhenNull {
208+
}
209+
210+
@ImportHttpServices(group = "test", groupProvider = FromClassGroupProvider.class)
211+
static class InvalidProvidedGroupNameConfig {
212+
}
213+
214+
static class FromClassGroupProvider implements GroupProvider {
215+
216+
@Override
217+
public @Nullable String group(AnnotationMetadata metadata) {
218+
String shortName = ClassUtils.getShortName(metadata.getClassName());
219+
return shortName.substring(0, shortName.length()-1).toLowerCase();
220+
}
221+
}
222+
223+
static class FromClassGroupProviderWithoutAs extends FromClassGroupProvider {
224+
225+
@Override
226+
public @Nullable String group(AnnotationMetadata metadata) {
227+
return (metadata.getClassName().endsWith("A")) ? null : super.group(metadata);
228+
}
229+
}
230+
165231
}

0 commit comments

Comments
 (0)