Skip to content

Commit 279bce7

Browse files
committed
Add HttpServiceClient and registrar
See gh-35244
1 parent da44302 commit 279bce7

File tree

7 files changed

+296
-5
lines changed

7 files changed

+296
-5
lines changed

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Arrays;
2020
import java.util.Objects;
21+
import java.util.stream.Stream;
2122

2223
import org.jspecify.annotations.Nullable;
2324

@@ -186,15 +187,16 @@ private RootBeanDefinition createOrGetRegistry(BeanDefinitionRegistry beanRegist
186187
protected abstract void registerHttpServices(
187188
GroupRegistry registry, AnnotationMetadata importingClassMetadata);
188189

189-
private ClassPathScanningCandidateComponentProvider getScanner() {
190+
191+
protected Stream<BeanDefinition> findHttpServices(String basePackage) {
190192
if (this.scanner == null) {
191193
Assert.state(this.environment != null, "Environment has not been set");
192194
Assert.state(this.resourceLoader != null, "ResourceLoader has not been set");
193195
this.scanner = new HttpExchangeClassPathScanningCandidateComponentProvider();
194196
this.scanner.setEnvironment(this.environment);
195197
this.scanner.setResourceLoader(this.resourceLoader);
196198
}
197-
return this.scanner;
199+
return this.scanner.findCandidateComponents(basePackage).stream();
198200
}
199201

200202
private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) {
@@ -244,10 +246,15 @@ default GroupSpec forDefaultGroup() {
244246
interface GroupSpec {
245247

246248
/**
247-
* List HTTP Service types to create proxies for.
249+
* Register HTTP Service types to create proxies for.
248250
*/
249251
GroupSpec register(Class<?>... serviceTypes);
250252

253+
/**
254+
* Register HTTP Service types using fully qualified type names.
255+
*/
256+
GroupSpec registerTypeNames(String... serviceTypes);
257+
251258
/**
252259
* Detect HTTP Service types in the given packages, looking for
253260
* interfaces with a type and/or method {@link HttpExchange} annotation.
@@ -258,7 +265,6 @@ interface GroupSpec {
258265
* Variant of {@link #detectInBasePackages(Class[])} with a String package name.
259266
*/
260267
GroupSpec detectInBasePackages(String... packageNames);
261-
262268
}
263269
}
264270

@@ -288,6 +294,12 @@ public GroupRegistry.GroupSpec register(Class<?>... serviceTypes) {
288294
return this;
289295
}
290296

297+
@Override
298+
public GroupRegistry.GroupSpec registerTypeNames(String... serviceTypes) {
299+
Arrays.stream(serviceTypes).forEach(this::registerServiceTypeName);
300+
return this;
301+
}
302+
291303
@Override
292304
public GroupRegistry.GroupSpec detectInBasePackages(Class<?>... packageClasses) {
293305
Arrays.stream(packageClasses).map(Class::getPackageName).forEach(this::detectInBasePackage);
@@ -301,7 +313,7 @@ public GroupRegistry.GroupSpec detectInBasePackages(String... packageNames) {
301313
}
302314

303315
private void detectInBasePackage(String packageName) {
304-
getScanner().findCandidateComponents(packageName).stream()
316+
findHttpServices(packageName)
305317
.map(BeanDefinition::getBeanClassName)
306318
.filter(Objects::nonNull)
307319
.forEach(this::registerServiceTypeName);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-present 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+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
import org.springframework.core.annotation.AliasFor;
27+
28+
/**
29+
* Annotation to mark an HTTP Service interface as a candidate client proxy creation.
30+
* Supported by extensions of {@link HttpServiceClientRegistrarSupport}.
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 7.0
34+
* @see HttpServiceClientRegistrarSupport
35+
*/
36+
@Target(ElementType.TYPE)
37+
@Retention(RetentionPolicy.RUNTIME)
38+
@Documented
39+
public @interface HttpServiceClient {
40+
41+
/**
42+
* An alias for {@link #group()}.
43+
*/
44+
@AliasFor("group")
45+
String value() default HttpServiceGroup.DEFAULT_GROUP_NAME;
46+
47+
/**
48+
* The name of the HTTP Service group for this client.
49+
* <p>By default, this is {@link HttpServiceGroup#DEFAULT_GROUP_NAME}.
50+
*/
51+
@AliasFor("value")
52+
String group() default HttpServiceGroup.DEFAULT_GROUP_NAME;
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-present 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+
20+
import java.util.List;
21+
22+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
23+
import org.springframework.core.annotation.MergedAnnotations;
24+
import org.springframework.core.type.AnnotationMetadata;
25+
26+
/**
27+
* Base class for an {@link AbstractHttpServiceRegistrar} to detects and register
28+
* {@link HttpServiceClient @HttpServiceClient} annotated interfaces.
29+
*
30+
* <p>Subclasses need to implement
31+
* {@link #registerHttpServices(GroupRegistry, AnnotationMetadata)} and invoke
32+
* {@link #findAndRegisterHttpServiceClients(GroupRegistry, List)}.
33+
*
34+
* @author Rossen Stoyanchev
35+
* @since 7.0
36+
*/
37+
public abstract class HttpServiceClientRegistrarSupport extends AbstractHttpServiceRegistrar {
38+
39+
/**
40+
* Find all HTTP Services under the given base packages that also have an
41+
* {@link HttpServiceClient @HttpServiceClient} annotation, and register them
42+
* in the group specified on the annotation.
43+
* @param registry the registry from {@link #registerHttpServices(GroupRegistry, AnnotationMetadata)}
44+
* @param basePackages the base packages to scan
45+
*/
46+
protected void findAndRegisterHttpServiceClients(GroupRegistry registry, List<String> basePackages) {
47+
basePackages.stream()
48+
.flatMap(this::findHttpServices)
49+
.filter(definition -> definition instanceof AnnotatedBeanDefinition)
50+
.map(definition -> (AnnotatedBeanDefinition) definition)
51+
.filter(definition -> definition.getMetadata().hasAnnotation(HttpServiceClient.class.getName()))
52+
.filter(definition -> definition.getBeanClassName() != null)
53+
.forEach(definition -> {
54+
MergedAnnotations annotations = definition.getMetadata().getAnnotations();
55+
String group = annotations.get(HttpServiceClient.class).getString("group");
56+
registry.forGroup(group).registerTypeNames(definition.getBeanClassName());
57+
});
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2002-present 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+
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.core.env.StandardEnvironment;
26+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
27+
import org.springframework.core.type.AnnotationMetadata;
28+
import org.springframework.web.service.registry.client.DefaultClient;
29+
import org.springframework.web.service.registry.client.EchoClientA;
30+
import org.springframework.web.service.registry.client.EchoClientB;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.mockito.Mockito.mock;
34+
35+
/**
36+
* Unit tests for {@link HttpServiceClientRegistrarSupport}.
37+
* @author Rossen Stoyanchev
38+
*/
39+
public class HttpServiceClientRegistrarSupportTests {
40+
41+
private final TestGroupRegistry groupRegistry = new TestGroupRegistry();
42+
43+
44+
@Test
45+
void register() {
46+
HttpServiceClientRegistrarSupport registrar = new HttpServiceClientRegistrarSupport() {
47+
48+
@Override
49+
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
50+
findAndRegisterHttpServiceClients(groupRegistry, List.of(getClass().getPackage().getName() + ".client"));
51+
}
52+
};
53+
registrar.setEnvironment(new StandardEnvironment());
54+
registrar.setResourceLoader(new PathMatchingResourcePatternResolver());
55+
56+
registrar.registerHttpServices(groupRegistry, mock(AnnotationMetadata.class));
57+
58+
assertGroups(
59+
TestGroup.ofListing("echo", EchoClientA.class, EchoClientB.class),
60+
TestGroup.ofListing("default", DefaultClient.class));
61+
}
62+
63+
private void assertGroups(TestGroup... expectedGroups) {
64+
Map<String, TestGroup> groupMap = this.groupRegistry.groupMap();
65+
assertThat(groupMap.size()).isEqualTo(expectedGroups.length);
66+
for (TestGroup expected : expectedGroups) {
67+
TestGroup actual = groupMap.get(expected.name());
68+
assertThat(actual.httpServiceTypes()).isEqualTo(expected.httpServiceTypes());
69+
assertThat(actual.clientType()).isEqualTo(expected.clientType());
70+
assertThat(actual.packageNames()).isEqualTo(expected.packageNames());
71+
assertThat(actual.packageClasses()).isEqualTo(expected.packageClasses());
72+
}
73+
}
74+
75+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2002-present 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.client;
18+
19+
20+
import org.springframework.web.bind.annotation.RequestParam;
21+
import org.springframework.web.service.annotation.GetExchange;
22+
import org.springframework.web.service.registry.HttpServiceClient;
23+
24+
@HttpServiceClient
25+
public interface DefaultClient {
26+
27+
@GetExchange
28+
String handle(@RequestParam String input);
29+
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2002-present 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.client;
18+
19+
20+
import org.springframework.web.bind.annotation.RequestParam;
21+
import org.springframework.web.service.annotation.GetExchange;
22+
import org.springframework.web.service.registry.HttpServiceClient;
23+
24+
@HttpServiceClient("echo")
25+
public interface EchoClientA {
26+
27+
@GetExchange
28+
String handle(@RequestParam String input);
29+
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2002-present 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.client;
18+
19+
20+
import org.springframework.web.bind.annotation.RequestParam;
21+
import org.springframework.web.service.annotation.GetExchange;
22+
import org.springframework.web.service.registry.HttpServiceClient;
23+
24+
@HttpServiceClient("echo")
25+
public interface EchoClientB {
26+
27+
@GetExchange
28+
String handle(@RequestParam String input);
29+
30+
}

0 commit comments

Comments
 (0)