diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index 570a2d3cd9..d7d9b461b0 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -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 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); diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClient.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClient.java new file mode 100644 index 0000000000..56fe47a93f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClient.java @@ -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. + *

By default, this is {@link HttpServiceGroup#DEFAULT_GROUP_NAME}. + */ + @AliasFor("value") + String group() default HttpServiceGroup.DEFAULT_GROUP_NAME; + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupport.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupport.java new file mode 100644 index 0000000000..ad9d22d2e5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupport.java @@ -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. + * + *

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 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()); + }); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupportTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupportTests.java new file mode 100644 index 0000000000..ef5b11a952 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceClientRegistrarSupportTests.java @@ -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 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()); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/client/DefaultClient.java b/spring-web/src/test/java/org/springframework/web/service/registry/client/DefaultClient.java new file mode 100644 index 0000000000..197f950873 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/client/DefaultClient.java @@ -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); + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientA.java b/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientA.java new file mode 100644 index 0000000000..e339f9a35d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientA.java @@ -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); + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientB.java b/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientB.java new file mode 100644 index 0000000000..ed28b908ff --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/client/EchoClientB.java @@ -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); + +}