Add HttpServiceClient and registrar

See gh-35244
This commit is contained in:
rstoyanchev 2025-07-30 13:26:54 +01:00
parent da443020e0
commit 279bce7124
7 changed files with 296 additions and 5 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.web.service.registry;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
@ -186,7 +187,8 @@ public abstract class AbstractHttpServiceRegistrar implements
protected abstract void registerHttpServices(
GroupRegistry registry, AnnotationMetadata importingClassMetadata);
private ClassPathScanningCandidateComponentProvider getScanner() {
protected Stream<BeanDefinition> findHttpServices(String basePackage) {
if (this.scanner == null) {
Assert.state(this.environment != null, "Environment has not been set");
Assert.state(this.resourceLoader != null, "ResourceLoader has not been set");
@ -194,7 +196,7 @@ public abstract class AbstractHttpServiceRegistrar implements
this.scanner.setEnvironment(this.environment);
this.scanner.setResourceLoader(this.resourceLoader);
}
return this.scanner;
return this.scanner.findCandidateComponents(basePackage).stream();
}
private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) {
@ -244,10 +246,15 @@ public abstract class AbstractHttpServiceRegistrar implements
interface GroupSpec {
/**
* List HTTP Service types to create proxies for.
* Register HTTP Service types to create proxies for.
*/
GroupSpec register(Class<?>... serviceTypes);
/**
* Register HTTP Service types using fully qualified type names.
*/
GroupSpec registerTypeNames(String... serviceTypes);
/**
* Detect HTTP Service types in the given packages, looking for
* interfaces with a type and/or method {@link HttpExchange} annotation.
@ -258,7 +265,6 @@ public abstract class AbstractHttpServiceRegistrar implements
* Variant of {@link #detectInBasePackages(Class[])} with a String package name.
*/
GroupSpec detectInBasePackages(String... packageNames);
}
}
@ -288,6 +294,12 @@ public abstract class AbstractHttpServiceRegistrar implements
return this;
}
@Override
public GroupRegistry.GroupSpec registerTypeNames(String... serviceTypes) {
Arrays.stream(serviceTypes).forEach(this::registerServiceTypeName);
return this;
}
@Override
public GroupRegistry.GroupSpec detectInBasePackages(Class<?>... packageClasses) {
Arrays.stream(packageClasses).map(Class::getPackageName).forEach(this::detectInBasePackage);
@ -301,7 +313,7 @@ public abstract class AbstractHttpServiceRegistrar implements
}
private void detectInBasePackage(String packageName) {
getScanner().findCandidateComponents(packageName).stream()
findHttpServices(packageName)
.map(BeanDefinition::getBeanClassName)
.filter(Objects::nonNull)
.forEach(this::registerServiceTypeName);

View File

@ -0,0 +1,54 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Annotation to mark an HTTP Service interface as a candidate client proxy creation.
* Supported by extensions of {@link HttpServiceClientRegistrarSupport}.
*
* @author Rossen Stoyanchev
* @since 7.0
* @see HttpServiceClientRegistrarSupport
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpServiceClient {
/**
* An alias for {@link #group()}.
*/
@AliasFor("group")
String value() default HttpServiceGroup.DEFAULT_GROUP_NAME;
/**
* The name of the HTTP Service group for this client.
* <p>By default, this is {@link HttpServiceGroup#DEFAULT_GROUP_NAME}.
*/
@AliasFor("value")
String group() default HttpServiceGroup.DEFAULT_GROUP_NAME;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry;
import java.util.List;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.AnnotationMetadata;
/**
* Base class for an {@link AbstractHttpServiceRegistrar} to detects and register
* {@link HttpServiceClient @HttpServiceClient} annotated interfaces.
*
* <p>Subclasses need to implement
* {@link #registerHttpServices(GroupRegistry, AnnotationMetadata)} and invoke
* {@link #findAndRegisterHttpServiceClients(GroupRegistry, List)}.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
public abstract class HttpServiceClientRegistrarSupport extends AbstractHttpServiceRegistrar {
/**
* Find all HTTP Services under the given base packages that also have an
* {@link HttpServiceClient @HttpServiceClient} annotation, and register them
* in the group specified on the annotation.
* @param registry the registry from {@link #registerHttpServices(GroupRegistry, AnnotationMetadata)}
* @param basePackages the base packages to scan
*/
protected void findAndRegisterHttpServiceClients(GroupRegistry registry, List<String> basePackages) {
basePackages.stream()
.flatMap(this::findHttpServices)
.filter(definition -> definition instanceof AnnotatedBeanDefinition)
.map(definition -> (AnnotatedBeanDefinition) definition)
.filter(definition -> definition.getMetadata().hasAnnotation(HttpServiceClient.class.getName()))
.filter(definition -> definition.getBeanClassName() != null)
.forEach(definition -> {
MergedAnnotations annotations = definition.getMetadata().getAnnotations();
String group = annotations.get(HttpServiceClient.class).getString("group");
registry.forGroup(group).registerTypeNames(definition.getBeanClassName());
});
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.service.registry.client.DefaultClient;
import org.springframework.web.service.registry.client.EchoClientA;
import org.springframework.web.service.registry.client.EchoClientB;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link HttpServiceClientRegistrarSupport}.
* @author Rossen Stoyanchev
*/
public class HttpServiceClientRegistrarSupportTests {
private final TestGroupRegistry groupRegistry = new TestGroupRegistry();
@Test
void register() {
HttpServiceClientRegistrarSupport registrar = new HttpServiceClientRegistrarSupport() {
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
findAndRegisterHttpServiceClients(groupRegistry, List.of(getClass().getPackage().getName() + ".client"));
}
};
registrar.setEnvironment(new StandardEnvironment());
registrar.setResourceLoader(new PathMatchingResourcePatternResolver());
registrar.registerHttpServices(groupRegistry, mock(AnnotationMetadata.class));
assertGroups(
TestGroup.ofListing("echo", EchoClientA.class, EchoClientB.class),
TestGroup.ofListing("default", DefaultClient.class));
}
private void assertGroups(TestGroup... expectedGroups) {
Map<String, TestGroup> groupMap = this.groupRegistry.groupMap();
assertThat(groupMap.size()).isEqualTo(expectedGroups.length);
for (TestGroup expected : expectedGroups) {
TestGroup actual = groupMap.get(expected.name());
assertThat(actual.httpServiceTypes()).isEqualTo(expected.httpServiceTypes());
assertThat(actual.clientType()).isEqualTo(expected.clientType());
assertThat(actual.packageNames()).isEqualTo(expected.packageNames());
assertThat(actual.packageClasses()).isEqualTo(expected.packageClasses());
}
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry.client;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceClient;
@HttpServiceClient
public interface DefaultClient {
@GetExchange
String handle(@RequestParam String input);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry.client;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceClient;
@HttpServiceClient("echo")
public interface EchoClientA {
@GetExchange
String handle(@RequestParam String input);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.registry.client;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceClient;
@HttpServiceClient("echo")
public interface EchoClientB {
@GetExchange
String handle(@RequestParam String input);
}