Add HTTP Service registry support
See gh-33992
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support;
|
||||
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.service.invoker.HttpExchangeAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
|
||||
|
||||
/**
|
||||
* Adapter for groups backed by {@link RestClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class RestClientHttpServiceGroupAdapter implements HttpServiceGroupAdapter<RestClient.Builder> {
|
||||
|
||||
@Override
|
||||
public RestClient.Builder createClientBuilder() {
|
||||
return RestClient.builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends HttpServiceGroupConfigurer<RestClient.Builder>> getConfigurerType() {
|
||||
return RestClientHttpServiceGroupConfigurer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpExchangeAdapter createExchangeAdapter(RestClient.Builder clientBuilder) {
|
||||
return RestClientAdapter.create(clientBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support;
|
||||
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
|
||||
|
||||
/**
|
||||
* Extension of {@link HttpServiceGroupConfigurer} to configure groups
|
||||
* with a {@link RestClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface RestClientHttpServiceGroupConfigurer extends HttpServiceGroupConfigurer<RestClient.Builder> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
|
||||
/**
|
||||
* Abstract registrar class that imports:
|
||||
* <ul>
|
||||
* <li>Bean definitions for HTTP Service interface client proxies organized by
|
||||
* {@link HttpServiceGroup}.
|
||||
* <li>Bean definition for an {@link HttpServiceProxyRegistryFactoryBean} that
|
||||
* initializes the infrastructure for each group, {@code RestClient} or
|
||||
* {@code WebClient} and a proxy factory, necessary to create the proxies.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Subclasses determine the HTTP Service types (interfaces with
|
||||
* {@link HttpExchange @HttpExchange} methods) to register by implementing
|
||||
* {@link #registerHttpServices}.
|
||||
*
|
||||
* <p>There is built-in support for declaring HTTP Services through
|
||||
* {@link ImportHttpServices} annotations. It is also possible to perform
|
||||
* registrations directly, sourced in another way, by extending this class.
|
||||
*
|
||||
* <p>It is possible to import multiple instances of this registrar type.
|
||||
* Subsequent imports update the existing registry {@code FactoryBean}
|
||||
* definition, and likewise merge HTTP Service group definitions.
|
||||
*
|
||||
* <p>An application can autowire HTTP Service proxy beans, or autowire the
|
||||
* {@link HttpServiceProxyRegistry} from which to obtain proxies.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see ImportHttpServices
|
||||
* @see HttpServiceProxyRegistryFactoryBean
|
||||
*/
|
||||
public abstract class AbstractHttpServiceRegistrar implements
|
||||
ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware {
|
||||
|
||||
private HttpServiceGroup.ClientType defaultClientType = HttpServiceGroup.ClientType.UNSPECIFIED;
|
||||
|
||||
private @Nullable Environment environment;
|
||||
|
||||
private @Nullable ResourceLoader resourceLoader;
|
||||
|
||||
private @Nullable BeanFactory beanFactory;
|
||||
|
||||
private final Map<String, HttpServiceGroup> groupMap = new LinkedHashMap<>();
|
||||
|
||||
private @Nullable ClassPathScanningCandidateComponentProvider scanner;
|
||||
|
||||
|
||||
/**
|
||||
* Set the client type to use when the client type for an HTTP Service group
|
||||
* remains {@link HttpServiceGroup.ClientType#UNSPECIFIED}.
|
||||
* <p>By default, when this property is not set, then {@code REST_CLIENT}
|
||||
* is used for any HTTP Service group whose client type remains unspecified.
|
||||
*/
|
||||
public void setDefaultClientType(HttpServiceGroup.ClientType defaultClientType) {
|
||||
this.defaultClientType = defaultClientType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public final void registerBeanDefinitions(
|
||||
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry beanRegistry,
|
||||
BeanNameGenerator beanNameGenerator) {
|
||||
|
||||
registerHttpServices(new DefaultGroupRegistry(), importingClassMetadata);
|
||||
|
||||
String proxyRegistryBeanName = HttpServiceProxyRegistry.class.getName();
|
||||
GenericBeanDefinition proxyRegistryBeanDef;
|
||||
|
||||
if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) {
|
||||
proxyRegistryBeanDef = new GenericBeanDefinition();
|
||||
proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class);
|
||||
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
|
||||
args.addIndexedArgumentValue(0, new LinkedHashMap<String, HttpServiceGroup>());
|
||||
beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef);
|
||||
}
|
||||
else {
|
||||
proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName);
|
||||
}
|
||||
|
||||
mergeHttpServices(proxyRegistryBeanDef);
|
||||
|
||||
this.groupMap.forEach((groupName, group) -> group.httpServiceTypes().forEach(type -> {
|
||||
GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition();
|
||||
proxyBeanDef.setBeanClass(type);
|
||||
proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type));
|
||||
String beanName = (groupName + "." + beanNameGenerator.generateBeanName(proxyBeanDef, beanRegistry));
|
||||
if (!beanRegistry.containsBeanDefinition(beanName)) {
|
||||
beanRegistry.registerBeanDefinition(beanName, proxyBeanDef);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before any bean definition registrations are made.
|
||||
* Subclasses must implement it to register the HTTP Services for which bean
|
||||
* definitions for which proxies need to be created.
|
||||
* @param registry to perform HTTP Service registrations with
|
||||
* @param importingClassMetadata annotation metadata of the importing class
|
||||
*/
|
||||
protected abstract void registerHttpServices(
|
||||
GroupRegistry registry, AnnotationMetadata importingClassMetadata);
|
||||
|
||||
private ClassPathScanningCandidateComponentProvider getScanner() {
|
||||
if (this.scanner == null) {
|
||||
Assert.state(environment != null, "Environment has not been set");
|
||||
Assert.state(resourceLoader != null, "ResourceLoader has not been set");
|
||||
this.scanner = new HttpExchangeClassPathScanningCandidateComponentProvider();
|
||||
this.scanner.setEnvironment(this.environment);
|
||||
this.scanner.setResourceLoader(this.resourceLoader);
|
||||
}
|
||||
return this.scanner;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void mergeHttpServices(GenericBeanDefinition proxyRegistryBeanDef) {
|
||||
ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues();
|
||||
ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class);
|
||||
Assert.state(valueHolder != null, "Expected Map constructor argument at index 0");
|
||||
Map<String, HttpServiceGroup> targetMap = (Map<String, HttpServiceGroup>) valueHolder.getValue();
|
||||
Assert.state(targetMap != null, "No constructor argument value");
|
||||
|
||||
this.groupMap.forEach((name, group) -> {
|
||||
HttpServiceGroup previousGroup = targetMap.putIfAbsent(name, group);
|
||||
if (previousGroup != null) {
|
||||
if (!compatibleClientTypes(group.clientType(), previousGroup.clientType())) {
|
||||
throw new IllegalArgumentException("ClientType conflict for group '" + name + "'");
|
||||
}
|
||||
previousGroup.httpServiceTypes().addAll(group.httpServiceTypes());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean compatibleClientTypes(
|
||||
HttpServiceGroup.ClientType clientTypeA, HttpServiceGroup.ClientType clientTypeB) {
|
||||
|
||||
return (clientTypeA == clientTypeB ||
|
||||
clientTypeA == HttpServiceGroup.ClientType.UNSPECIFIED ||
|
||||
clientTypeB == HttpServiceGroup.ClientType.UNSPECIFIED);
|
||||
}
|
||||
|
||||
private Object getProxyInstance(String registryBeanName, String groupName, Class<?> type) {
|
||||
Assert.state(this.beanFactory != null, "BeanFactory has not been set");
|
||||
HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class);
|
||||
Object proxy = registry.getClient(groupName, type);
|
||||
Assert.notNull(proxy, "No proxy for HTTP Service [" + type.getName() + "]");
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registry API to allow subclasses to register HTTP Services.
|
||||
*/
|
||||
protected interface GroupRegistry {
|
||||
|
||||
/**
|
||||
* Perform HTTP Service registrations for the given group.
|
||||
*/
|
||||
GroupSpec forGroup(String name);
|
||||
|
||||
/**
|
||||
* Variant of {@link #forGroup(String)} with a client type.
|
||||
*/
|
||||
GroupSpec forGroup(String name, HttpServiceGroup.ClientType clientType);
|
||||
|
||||
/**
|
||||
* Perform HTTP Service registrations for the
|
||||
* {@link HttpServiceGroup#DEFAULT_GROUP_NAME} group.
|
||||
*/
|
||||
default GroupSpec forDefaultGroup() {
|
||||
return forGroup(HttpServiceGroup.DEFAULT_GROUP_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec to list or scan for HTTP Service types.
|
||||
*/
|
||||
interface GroupSpec {
|
||||
|
||||
/**
|
||||
* List HTTP Service types to create proxies for.
|
||||
*/
|
||||
GroupSpec register(Class<?>... serviceTypes);
|
||||
|
||||
/**
|
||||
* Detect HTTP Service types in the given packages, looking for
|
||||
* interfaces with a type and/or method {@link HttpExchange} annotation.
|
||||
*/
|
||||
GroupSpec detectInBasePackages(Class<?>... packageClasses);
|
||||
|
||||
/**
|
||||
* Variant of {@link #detectInBasePackages(Class[])} with a String package name.
|
||||
*/
|
||||
GroupSpec detectInBasePackages(String... packageNames);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GroupRegistry}.
|
||||
*/
|
||||
private class DefaultGroupRegistry implements GroupRegistry {
|
||||
|
||||
@Override
|
||||
public GroupSpec forGroup(String name) {
|
||||
return forGroup(name, HttpServiceGroup.ClientType.UNSPECIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupSpec forGroup(String name, HttpServiceGroup.ClientType clientType) {
|
||||
return new DefaultGroupSpec(name, clientType);
|
||||
}
|
||||
|
||||
private class DefaultGroupSpec implements GroupSpec {
|
||||
|
||||
private final String groupName;
|
||||
|
||||
private final HttpServiceGroup.ClientType clientType;
|
||||
|
||||
public DefaultGroupSpec(String groupName, HttpServiceGroup.ClientType clientType) {
|
||||
this.groupName = groupName;
|
||||
this.clientType = initClientType(clientType);
|
||||
}
|
||||
|
||||
private HttpServiceGroup.ClientType initClientType(HttpServiceGroup.ClientType clientType) {
|
||||
if (clientType != HttpServiceGroup.ClientType.UNSPECIFIED) {
|
||||
return clientType;
|
||||
}
|
||||
else if (defaultClientType != HttpServiceGroup.ClientType.UNSPECIFIED) {
|
||||
return defaultClientType;
|
||||
}
|
||||
else {
|
||||
return HttpServiceGroup.ClientType.REST_CLIENT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupSpec register(Class<?>... serviceTypes) {
|
||||
addHttpServiceTypes(groupName, clientType, serviceTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupSpec detectInBasePackages(Class<?>... packageClasses) {
|
||||
for (Class<?> packageClass : packageClasses) {
|
||||
detect(groupName, clientType, packageClass.getPackageName());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupSpec detectInBasePackages(String... packageNames) {
|
||||
for (String packageName : packageNames) {
|
||||
detect(groupName, clientType, packageName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void detect(String groupName, HttpServiceGroup.ClientType clientType, String packageName) {
|
||||
for (BeanDefinition definition : getScanner().findCandidateComponents(packageName)) {
|
||||
String className = definition.getBeanClassName();
|
||||
if (className != null) {
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(className, getClass().getClassLoader());
|
||||
addHttpServiceTypes(groupName, clientType, clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException("Failed to load '" + className + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addHttpServiceTypes(
|
||||
String groupName, HttpServiceGroup.ClientType clientType, Class<?>... serviceTypes) {
|
||||
|
||||
groupMap.computeIfAbsent(groupName, name -> new RegisteredGroup(name, new LinkedHashSet<>(), clientType))
|
||||
.httpServiceTypes().addAll(Arrays.asList(serviceTypes));
|
||||
}
|
||||
}
|
||||
|
||||
private record RegisteredGroup(
|
||||
String name, Set<Class<?>> httpServiceTypes, ClientType clientType) implements HttpServiceGroup {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extension of ClassPathScanningCandidateComponentProvider to look for HTTP Services.
|
||||
*/
|
||||
private static class HttpExchangeClassPathScanningCandidateComponentProvider
|
||||
extends ClassPathScanningCandidateComponentProvider {
|
||||
|
||||
public HttpExchangeClassPathScanningCandidateComponentProvider() {
|
||||
addIncludeFilter(new HttpExchangeFilter());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
|
||||
AnnotationMetadata metadata = beanDefinition.getMetadata();
|
||||
return (metadata.isIndependent() && !metadata.isAnnotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find interfaces with type and/or method {@code @HttpExchange}.
|
||||
*/
|
||||
private static class HttpExchangeFilter extends AnnotationTypeFilter {
|
||||
|
||||
public HttpExchangeFilter() {
|
||||
super(HttpExchange.class, true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchSelf(MetadataReader metadataReader) {
|
||||
if (metadataReader.getClassMetadata().isInterface()) {
|
||||
for (MethodMetadata metadata : metadataReader.getAnnotationMetadata().getDeclaredMethods()) {
|
||||
if (metadata.getAnnotations().isPresent(HttpExchange.class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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 org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* Built-in implementation {@link AbstractHttpServiceRegistrar} that uses
|
||||
* {@link ImportHttpServices} annotations on the importing configuration class
|
||||
* to determine the HTTP services and groups to register.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
final class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar {
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) {
|
||||
|
||||
MergedAnnotation<?> groupsAnnot = importMetadata.getAnnotations().get(HttpServiceGroups.class);
|
||||
if (groupsAnnot.isPresent()) {
|
||||
HttpServiceGroup.ClientType clientType = groupsAnnot.getEnum("clientType", HttpServiceGroup.ClientType.class);
|
||||
for (MergedAnnotation<?> annot : groupsAnnot.getAnnotationArray("value", ImportHttpServices.class)) {
|
||||
processImportAnnotation(annot, registry, clientType);
|
||||
}
|
||||
}
|
||||
|
||||
importMetadata.getAnnotations().stream(ImportHttpServices.class).forEach(annot ->
|
||||
processImportAnnotation(annot, registry, HttpServiceGroup.ClientType.UNSPECIFIED));
|
||||
}
|
||||
|
||||
private void processImportAnnotation(
|
||||
MergedAnnotation<?> annotation, GroupRegistry groupRegistry,
|
||||
HttpServiceGroup.ClientType containerClientType) {
|
||||
|
||||
String groupName = annotation.getString("group");
|
||||
|
||||
HttpServiceGroup.ClientType clientType = annotation.getEnum("clientType", HttpServiceGroup.ClientType.class);
|
||||
clientType = (clientType != HttpServiceGroup.ClientType.UNSPECIFIED ? clientType : containerClientType);
|
||||
|
||||
groupRegistry.forGroup(groupName, clientType)
|
||||
.register(annotation.getClassArray("types"))
|
||||
.detectInBasePackages(annotation.getStringArray("basePackages"))
|
||||
.detectInBasePackages(annotation.getClassArray("basePackageClasses"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.Set;
|
||||
|
||||
/**
|
||||
* A group of HTTP Service interfaces that share the same
|
||||
* {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} and
|
||||
* HTTP client setup.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface HttpServiceGroup {
|
||||
|
||||
/**
|
||||
* The name of the group to add HTTP Services to when a group isn't specified.
|
||||
*/
|
||||
String DEFAULT_GROUP_NAME = "default";
|
||||
|
||||
|
||||
/**
|
||||
* The name of the HTTP Service group.
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* The HTTP Services in the group.
|
||||
*/
|
||||
Set<Class<?>> httpServiceTypes();
|
||||
|
||||
/**
|
||||
* The client type to use for the group.
|
||||
* <p>By default, {@link ClientType#REST_CLIENT} remains unspecified.
|
||||
*/
|
||||
ClientType clientType();
|
||||
|
||||
|
||||
/**
|
||||
* Enum to specify the client type to use for an HTTP Service group.
|
||||
*/
|
||||
enum ClientType {
|
||||
|
||||
/**
|
||||
* A group backed by {@link org.springframework.web.client.RestClient}.
|
||||
*/
|
||||
REST_CLIENT,
|
||||
|
||||
/**
|
||||
* A group backed by {@link org.springframework.web.reactive.function.client.WebClient}.
|
||||
*/
|
||||
WEB_CLIENT,
|
||||
|
||||
/**
|
||||
* Not specified, falling back on a default.
|
||||
* @see ImportHttpServices#clientType()
|
||||
* @see HttpServiceGroups#clientType()
|
||||
* @see AbstractHttpServiceRegistrar#setDefaultClientType
|
||||
*/
|
||||
UNSPECIFIED;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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 org.springframework.web.service.invoker.HttpExchangeAdapter;
|
||||
|
||||
/**
|
||||
* Adapter that helps to configure a group independent of its client builder type.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @param <CB> the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder.
|
||||
*/
|
||||
public interface HttpServiceGroupAdapter<CB> {
|
||||
|
||||
/**
|
||||
* Create a client builder instance.
|
||||
*/
|
||||
CB createClientBuilder();
|
||||
|
||||
/**
|
||||
* Return the type of configurer that is compatible with this group.
|
||||
*/
|
||||
Class<? extends HttpServiceGroupConfigurer<CB>> getConfigurerType();
|
||||
|
||||
/**
|
||||
* Use the client builder to create an {@link HttpExchangeAdapter} to use to
|
||||
* initialize the {@link org.springframework.web.service.invoker.HttpServiceProxyFactory}
|
||||
* for the group.
|
||||
*/
|
||||
HttpExchangeAdapter createExchangeAdapter(CB clientBuilder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
/**
|
||||
* Callback to configure the set of declared {@link HttpServiceGroup}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @param <CB> the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HttpServiceGroupConfigurer<CB> {
|
||||
|
||||
/**
|
||||
* Configure the underlying infrastructure for all group.
|
||||
*/
|
||||
void configureGroups(Groups<CB> groups);
|
||||
|
||||
|
||||
/**
|
||||
* Contract to help iterate and configure the set of groups.
|
||||
* @param <CB> the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder.
|
||||
*/
|
||||
interface Groups<CB> {
|
||||
|
||||
/**
|
||||
* Select groups to configure by name.
|
||||
*/
|
||||
Groups<CB> filterByName(String... groupNames);
|
||||
|
||||
/**
|
||||
* Select groups to configure through a {@link Predicate}.
|
||||
*/
|
||||
Groups<CB> filter(Predicate<HttpServiceGroup> predicate);
|
||||
|
||||
/**
|
||||
* Configure the client for the selected groups.
|
||||
* This is called once for each selected group.
|
||||
*/
|
||||
void configureClient(Consumer<CB> clientConfigurer);
|
||||
|
||||
/**
|
||||
* Variant of {@link #configureClient(Consumer)} with access to the
|
||||
* group being configured.
|
||||
*/
|
||||
void configureClient(BiConsumer<HttpServiceGroup, CB> clientConfigurer);
|
||||
|
||||
/**
|
||||
* Configure the {@link HttpServiceProxyFactory} for the selected groups.
|
||||
* This is called once for each selected group.
|
||||
*/
|
||||
void configureProxyFactory(BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer);
|
||||
|
||||
/**
|
||||
* Configure the client and {@link HttpServiceProxyFactory} for the selected groups.
|
||||
* This is called once for each selected group.
|
||||
*/
|
||||
void configure(BiConsumer<HttpServiceGroup, CB> clientConfigurer,
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Container annotation for the {@link ImportHttpServices} repeatable annotation.
|
||||
* Typically not necessary to use as {@code @ImportHttpServices} annotations can
|
||||
* be declared one after another without a wrapper, but the container annotation
|
||||
* may be used to set the {@link #clientType()} and that would be inherited by
|
||||
* all nested annotations.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(AnnotationHttpServiceRegistrar.class)
|
||||
public @interface HttpServiceGroups {
|
||||
|
||||
/**
|
||||
* Alias for {@link #groups()}.
|
||||
*/
|
||||
@AliasFor("groups")
|
||||
ImportHttpServices[] value() default {};
|
||||
|
||||
/**
|
||||
* Nested annotations that declare HTTP Services by group.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
ImportHttpServices[] groups() default {};
|
||||
|
||||
/**
|
||||
* Specify the type of client to use for nested {@link ImportHttpServices}
|
||||
* annotations that don't specify it.
|
||||
* <p>By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED}
|
||||
* in which case {@code RestClient} is used, but this default can be reset
|
||||
* via {@link AbstractHttpServiceRegistrar#setDefaultClientType}.
|
||||
*/
|
||||
HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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 org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A registry that contains HTTP Service client proxies.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see ImportHttpServices
|
||||
* @see HttpServiceProxyRegistryFactoryBean
|
||||
*/
|
||||
public interface HttpServiceProxyRegistry {
|
||||
|
||||
/**
|
||||
* Return an HTTP service client proxy from any group as long as there is
|
||||
* only one client proxy of the given type across all groups.
|
||||
* @param httpServiceType the type of client proxy
|
||||
* @return the proxy, or {@code null} if not found
|
||||
* @param <P> the type of HTTP Interface client proxy
|
||||
* @throws IllegalArgumentException if more than one client proxy of the
|
||||
* given type exists across groups
|
||||
*/
|
||||
<P> @Nullable P getClient(Class<P> httpServiceType);
|
||||
|
||||
/**
|
||||
* Return an HTTP service client proxy from the given group.
|
||||
* @param groupName the name of the group
|
||||
* @param httpServiceType the type of client proxy
|
||||
* @return the proxy, or {@code null} if not found
|
||||
* @param <P> the type of HTTP Interface client proxy
|
||||
*/
|
||||
<P> @Nullable P getClient(String groupName, Class<P> httpServiceType);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.service.invoker.HttpExchangeAdapter;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
/**
|
||||
* {@link FactoryBean} for {@link HttpServiceProxyRegistry} responsible for
|
||||
* initializing {@link HttpServiceGroup}s, and creating the HTTP Service client
|
||||
* proxies for each group.
|
||||
*
|
||||
* <p>This class is imported as a bean definition through an
|
||||
* {@link AbstractHttpServiceRegistrar}, and given .
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see AbstractHttpServiceRegistrar
|
||||
*/
|
||||
public final class HttpServiceProxyRegistryFactoryBean
|
||||
implements ApplicationContextAware, InitializingBean, FactoryBean<HttpServiceProxyRegistry> {
|
||||
|
||||
private final Set<ProxyHttpServiceGroup> groupSet;
|
||||
|
||||
private final Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> groupAdapters;
|
||||
|
||||
private @Nullable ApplicationContext applicationContext;
|
||||
|
||||
private @Nullable HttpServiceProxyRegistry proxyRegistry;
|
||||
|
||||
|
||||
HttpServiceProxyRegistryFactoryBean(Map<String, HttpServiceGroup> groupMap) {
|
||||
this.groupSet = groupMap.values().stream().map(ProxyHttpServiceGroup::new).collect(Collectors.toSet());
|
||||
this.groupAdapters = GroupAdapterInitializer.initGroupAdapters();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return HttpServiceProxyRegistry.class;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.notNull(this.applicationContext, "ApplicationContext not initialized");
|
||||
|
||||
// Set client builders
|
||||
groupAdapters.forEach((clientType, groupAdapter) -> {
|
||||
this.groupSet.stream()
|
||||
.filter(group -> group.clientType().equals(clientType))
|
||||
.forEach(group -> group.initialize(groupAdapter));
|
||||
});
|
||||
|
||||
// Apply group configurers
|
||||
groupAdapters.forEach((clientType, groupAdapter) -> {
|
||||
Collection<? extends HttpServiceGroupConfigurer<?>> configurers =
|
||||
this.applicationContext.getBeansOfType(groupAdapter.getConfigurerType()).values();
|
||||
|
||||
configurers.stream()
|
||||
.filter(configurer -> groupAdapter.getConfigurerType().isInstance(configurer))
|
||||
.forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType)));
|
||||
});
|
||||
|
||||
// Create proxies
|
||||
Map<String, Map<Class<?>, Object>> groupProxyMap = this.groupSet.stream()
|
||||
.collect(Collectors.toMap(ProxyHttpServiceGroup::name, ProxyHttpServiceGroup::createProxies));
|
||||
|
||||
this.proxyRegistry = new DefaultHttpServiceProxyRegistry(groupProxyMap);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public HttpServiceProxyRegistry getObject() {
|
||||
Assert.state(this.proxyRegistry != null, "HttpServiceProxyRegistry not initialized");
|
||||
return this.proxyRegistry;
|
||||
}
|
||||
|
||||
|
||||
private static class GroupAdapterInitializer {
|
||||
|
||||
static Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> initGroupAdapters() {
|
||||
Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> map = new LinkedHashMap<>(2);
|
||||
|
||||
addGroupAdapter(map, HttpServiceGroup.ClientType.REST_CLIENT,
|
||||
"org.springframework.web.client.support.RestClientHttpServiceGroupAdapter");
|
||||
|
||||
addGroupAdapter(map, HttpServiceGroup.ClientType.WEB_CLIENT,
|
||||
"org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupAdapter");
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void addGroupAdapter(
|
||||
Map<HttpServiceGroup.ClientType, HttpServiceGroupAdapter<?>> groupAdapters,
|
||||
HttpServiceGroup.ClientType clientType, String className) {
|
||||
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(className, HttpServiceGroupAdapter.class.getClassLoader());
|
||||
groupAdapters.put(clientType, (HttpServiceGroupAdapter<?>) BeanUtils.instantiateClass(clazz));
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link HttpServiceGroup} that creates client proxies.
|
||||
*/
|
||||
private static final class ProxyHttpServiceGroup implements HttpServiceGroup {
|
||||
|
||||
private final HttpServiceGroup declaredGroup;
|
||||
|
||||
private @Nullable Object clientBuilder;
|
||||
|
||||
private @Nullable HttpServiceGroupAdapter<?> groupAdapter;
|
||||
|
||||
private BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer = (group, builder) -> {};
|
||||
|
||||
ProxyHttpServiceGroup(HttpServiceGroup group) {
|
||||
this.declaredGroup = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.declaredGroup.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<?>> httpServiceTypes() {
|
||||
return this.declaredGroup.httpServiceTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientType clientType() {
|
||||
return this.declaredGroup.clientType();
|
||||
}
|
||||
|
||||
public <CB> void initialize(HttpServiceGroupAdapter<?> adapter) {
|
||||
this.clientBuilder = adapter.createClientBuilder();
|
||||
this.groupAdapter = adapter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <CB> void apply(
|
||||
BiConsumer<HttpServiceGroup, CB> clientConfigurer,
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
clientConfigurer.accept(this, (CB) this.clientBuilder);
|
||||
this.proxyFactoryConfigurer = this.proxyFactoryConfigurer.andThen(proxyFactoryConfigurer);
|
||||
}
|
||||
|
||||
public Map<Class<?>, Object> createProxies() {
|
||||
Map<Class<?>, Object> proxyMap = new LinkedHashMap<>(httpServiceTypes().size());
|
||||
HttpExchangeAdapter exchangeAdapter = initExchangeAdapter();
|
||||
HttpServiceProxyFactory.Builder proxyFactoryBuilder = HttpServiceProxyFactory.builderFor(exchangeAdapter);
|
||||
this.proxyFactoryConfigurer.accept(this, proxyFactoryBuilder);
|
||||
HttpServiceProxyFactory proxyFactory = proxyFactoryBuilder.build();
|
||||
httpServiceTypes().forEach(type -> proxyMap.put(type, proxyFactory.createClient(type)));
|
||||
return proxyMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <CB> HttpExchangeAdapter initExchangeAdapter() {
|
||||
Assert.state(this.clientBuilder != null, "Client builder not set");
|
||||
Assert.state(this.groupAdapter != null, "Group adapter not set");
|
||||
return ((HttpServiceGroupAdapter<CB>) this.groupAdapter).createExchangeAdapter((CB) this.clientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[id=" + name() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of Groups that helps to configure the set of declared groups.
|
||||
*/
|
||||
private final class DefaultGroups<CB> implements HttpServiceGroupConfigurer.Groups<CB> {
|
||||
|
||||
private final HttpServiceGroup.ClientType clientType;
|
||||
|
||||
private @Nullable Predicate<HttpServiceGroup> filter;
|
||||
|
||||
DefaultGroups(HttpServiceGroup.ClientType clientType) {
|
||||
this.clientType = clientType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpServiceGroupConfigurer.Groups<CB> filterByName(String... groupNames) {
|
||||
return filter(group -> Arrays.stream(groupNames).anyMatch(id -> id.equals(group.name())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpServiceGroupConfigurer.Groups<CB> filter(Predicate<HttpServiceGroup> predicate) {
|
||||
this.filter = (this.filter != null ? this.filter.or(predicate) : predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClient(Consumer<CB> clientConfigurer) {
|
||||
configureClient((group, builder) -> clientConfigurer.accept(builder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClient(BiConsumer<HttpServiceGroup, CB> clientConfigurer) {
|
||||
configure(clientConfigurer, (group, builder) -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureProxyFactory(
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
configure((group, builder) -> {}, proxyFactoryConfigurer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(
|
||||
BiConsumer<HttpServiceGroup, CB> clientConfigurer,
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
groupSet.stream()
|
||||
.filter(group -> group.clientType().equals(this.clientType))
|
||||
.filter(groups -> this.filter == null || this.filter.test(groups))
|
||||
.forEach(group -> group.apply(clientConfigurer, proxyFactoryConfigurer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link HttpServiceProxyRegistry} with a map of proxies.
|
||||
*/
|
||||
private static final class DefaultHttpServiceProxyRegistry implements HttpServiceProxyRegistry {
|
||||
|
||||
private final Map<String, Map<Class<?>, Object>> groupProxyMap;
|
||||
|
||||
private final MultiValueMap<Class<?>, Object> directLookupMap;
|
||||
|
||||
DefaultHttpServiceProxyRegistry(Map<String, Map<Class<?>, Object>> groupProxyMap) {
|
||||
this.groupProxyMap = groupProxyMap;
|
||||
this.directLookupMap = new LinkedMultiValueMap<>();
|
||||
groupProxyMap.values().forEach(map -> map.forEach(this.directLookupMap::add));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <P> @Nullable P getClient(Class<P> type) {
|
||||
List<Object> proxies = this.directLookupMap.getOrDefault(type, Collections.emptyList());
|
||||
Assert.state(proxies.size() <= 1, "No unique client of type " + type.getName());
|
||||
return (!proxies.isEmpty() ? (P) proxies.get(0) : null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <P> @Nullable P getClient(String groupName, Class<P> httpServiceType) {
|
||||
return (P) this.groupProxyMap.getOrDefault(groupName, Collections.emptyMap()).get(httpServiceType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
|
||||
/**
|
||||
* Annotation to identify HTTP Service types (interfaces with
|
||||
* {@link HttpExchange @HttpExchange} methods) for which to create client proxies,
|
||||
* and have those proxies registered as beans.
|
||||
*
|
||||
* <p>This is a repeatable annotation that is expected on
|
||||
* {@link org.springframework.context.annotation.Configuration @Configuration}
|
||||
* classes. Each annotation is associated with an {@link HttpServiceGroup}
|
||||
* identified by name through the {@link #group()} attribute.
|
||||
*
|
||||
* <p>The HTTP Services for each group can be listed via {@link #types()}, or
|
||||
* detected via {@link #basePackageClasses()} or {@link #basePackages()}.
|
||||
*
|
||||
* <p>An application can autowire HTTP Service proxy beans, or autowire the
|
||||
* {@link HttpServiceProxyRegistry} from which to obtain proxies.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see HttpServiceGroups
|
||||
* @see AbstractHttpServiceRegistrar
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Repeatable(HttpServiceGroups.class)
|
||||
@Import(AnnotationHttpServiceRegistrar.class)
|
||||
@Documented
|
||||
public @interface ImportHttpServices {
|
||||
|
||||
/**
|
||||
* An alias for {@link #types()}.
|
||||
*/
|
||||
@AliasFor("types")
|
||||
Class<?>[] value() default {};
|
||||
|
||||
/**
|
||||
* A list of HTTP Service types to include in the group.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
Class<?>[] types() default {};
|
||||
|
||||
/**
|
||||
* The name of the HTTP Service group.
|
||||
* <p>If not specified, declared HTTP Services are grouped under the
|
||||
* {@link HttpServiceGroup#DEFAULT_GROUP_NAME}.
|
||||
*/
|
||||
String group() default HttpServiceGroup.DEFAULT_GROUP_NAME;
|
||||
|
||||
/**
|
||||
* Detect HTTP Services in the packages of the specified classes by looking
|
||||
* for interfaces with type or method level
|
||||
* {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}.
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
|
||||
/**
|
||||
* Variant of {@link #basePackageClasses()} with a list of packages
|
||||
* specified by package name.
|
||||
*/
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Specify the type of client to use for the group.
|
||||
* <p>By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED}
|
||||
* in which case {@code RestClient} is used, but this default can be reset
|
||||
* via {@link AbstractHttpServiceRegistrar#setDefaultClientType}.
|
||||
*/
|
||||
HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Support for creating a registry of HTTP Service client proxies, and declaring
|
||||
* the proxies as beans.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.web.service.registry;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
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.type.AnnotationMetadata;
|
||||
import org.springframework.web.client.support.echo.EchoA;
|
||||
import org.springframework.web.client.support.echo.EchoB;
|
||||
import org.springframework.web.client.support.greeting.GreetingA;
|
||||
import org.springframework.web.client.support.greeting.GreetingB;
|
||||
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyRegistry} with a
|
||||
* {@link org.springframework.web.client.RestClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class RestClientProxyRegistryIntegrationTests {
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
this.server.start(9090);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = {
|
||||
ListingConfig.class, DetectConfig.class, ManualListingConfig.class, ManualDetectionConfig.class
|
||||
})
|
||||
void basic(Class<?> configClass) throws InterruptedException {
|
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass);
|
||||
|
||||
EchoA echoA = context.getBean(EchoA.class);
|
||||
EchoB echoB = context.getBean(EchoB.class);
|
||||
|
||||
GreetingA greetingA = context.getBean(GreetingA.class);
|
||||
GreetingB greetingB = context.getBean(GreetingB.class);
|
||||
|
||||
HttpServiceProxyRegistry registry = context.getBean(HttpServiceProxyRegistry.class);
|
||||
|
||||
assertThat(registry.getClient(EchoA.class)).isSameAs(echoA);
|
||||
assertThat(registry.getClient(EchoB.class)).isSameAs(echoB);
|
||||
|
||||
assertThat(registry.getClient(GreetingA.class)).isSameAs(greetingA);
|
||||
assertThat(registry.getClient(GreetingB.class)).isSameAs(greetingB);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this.server.enqueue(new MockResponse().setBody("body"));
|
||||
}
|
||||
|
||||
echoA.handle("a");
|
||||
echoB.handle("b");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/echoA?input=a");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/echoB?input=b");
|
||||
|
||||
greetingA.handle("a");
|
||||
greetingB.handle("b");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/greetingA?input=a");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/greetingB?input=b");
|
||||
}
|
||||
|
||||
|
||||
private static class ClientConfig {
|
||||
|
||||
@Bean
|
||||
public RestClientHttpServiceGroupConfigurer groupConfigurer() {
|
||||
return groups -> groups.filterByName("echo", "greeting")
|
||||
.configureClient((group, builder) -> builder.baseUrl("http://localhost:9090"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class})
|
||||
@ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class})
|
||||
private static class ListingConfig extends ClientConfig {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportHttpServices(group = "echo", basePackageClasses = EchoA.class)
|
||||
@ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class)
|
||||
private static class DetectConfig extends ClientConfig {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(ManualListingRegistrar.class)
|
||||
private static class ManualListingConfig extends ClientConfig {
|
||||
}
|
||||
|
||||
private static class ManualListingRegistrar extends AbstractHttpServiceRegistrar {
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
|
||||
registry.forGroup("echo").register(EchoA.class, EchoB.class);
|
||||
registry.forGroup("greeting").register(GreetingA.class, GreetingB.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(ManualDetectionRegistrar.class)
|
||||
private static class ManualDetectionConfig extends ClientConfig {
|
||||
}
|
||||
|
||||
private static class ManualDetectionRegistrar extends AbstractHttpServiceRegistrar {
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
|
||||
registry.forGroup("echo").detectInBasePackages(EchoA.class);
|
||||
registry.forGroup("greeting").detectInBasePackages(GreetingA.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support.echo;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface EchoA {
|
||||
|
||||
@GetExchange("/echoA")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support.echo;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface EchoB {
|
||||
|
||||
@GetExchange("/echoB")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support.greeting;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface GreetingA {
|
||||
|
||||
@GetExchange("/greetingA")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.client.support.greeting;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface GreetingB {
|
||||
|
||||
@GetExchange("/greetingB")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support;
|
||||
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.service.invoker.HttpExchangeAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
|
||||
|
||||
/**
|
||||
* Adapter for groups backed by {@link WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class WebClientHttpServiceGroupAdapter implements HttpServiceGroupAdapter<WebClient.Builder> {
|
||||
|
||||
@Override
|
||||
public WebClient.Builder createClientBuilder() {
|
||||
return WebClient.builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends HttpServiceGroupConfigurer<WebClient.Builder>> getConfigurerType() {
|
||||
return WebClientHttpServiceGroupConfigurer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpExchangeAdapter createExchangeAdapter(WebClient.Builder clientBuilder) {
|
||||
return WebClientAdapter.create(clientBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support;
|
||||
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
|
||||
|
||||
/**
|
||||
* Extension of {@link HttpServiceGroupConfigurer} to configure groups
|
||||
* with a {@link WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface WebClientHttpServiceGroupConfigurer extends HttpServiceGroupConfigurer<WebClient.Builder> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
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.type.AnnotationMetadata;
|
||||
import org.springframework.web.reactive.function.client.support.echo.EchoA;
|
||||
import org.springframework.web.reactive.function.client.support.echo.EchoB;
|
||||
import org.springframework.web.reactive.function.client.support.greeting.GreetingA;
|
||||
import org.springframework.web.reactive.function.client.support.greeting.GreetingB;
|
||||
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
|
||||
import org.springframework.web.service.registry.HttpServiceGroups;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyRegistry} with a
|
||||
* {@link org.springframework.web.reactive.function.client.WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebClientProxyRegistryIntegrationTests {
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
this.server.start(9090);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = {
|
||||
ListingConfig.class, DetectConfig.class, ManualListingConfig.class, ManualDetectionConfig.class
|
||||
})
|
||||
void basic(Class<?> configClass) throws InterruptedException {
|
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass);
|
||||
|
||||
EchoA echoA = context.getBean(EchoA.class);
|
||||
EchoB echoB = context.getBean(EchoB.class);
|
||||
|
||||
GreetingA greetingA = context.getBean(GreetingA.class);
|
||||
GreetingB greetingB = context.getBean(GreetingB.class);
|
||||
|
||||
HttpServiceProxyRegistry registry = context.getBean(HttpServiceProxyRegistry.class);
|
||||
|
||||
assertThat(registry.getClient(EchoA.class)).isSameAs(echoA);
|
||||
assertThat(registry.getClient(EchoB.class)).isSameAs(echoB);
|
||||
|
||||
assertThat(registry.getClient(GreetingA.class)).isSameAs(greetingA);
|
||||
assertThat(registry.getClient(GreetingB.class)).isSameAs(greetingB);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this.server.enqueue(new MockResponse().setBody("body"));
|
||||
}
|
||||
|
||||
echoA.handle("a");
|
||||
echoB.handle("b");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/echoA?input=a");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/echoB?input=b");
|
||||
|
||||
greetingA.handle("a");
|
||||
greetingB.handle("b");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/greetingA?input=a");
|
||||
|
||||
request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/greetingB?input=b");
|
||||
}
|
||||
|
||||
|
||||
private static class BaseEchoConfig {
|
||||
|
||||
@Bean
|
||||
public WebClientHttpServiceGroupConfigurer groupConfigurer() {
|
||||
return groups -> groups.filterByName("echo", "greeting")
|
||||
.configureClient((group, builder) -> builder.baseUrl("http://localhost:9090"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = {
|
||||
@ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class}),
|
||||
@ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class})
|
||||
})
|
||||
private static class ListingConfig extends BaseEchoConfig {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = {
|
||||
@ImportHttpServices(group = "echo", basePackageClasses = EchoA.class),
|
||||
@ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class)
|
||||
})
|
||||
private static class DetectConfig extends BaseEchoConfig {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(ManualListingRegistrar.class)
|
||||
private static class ManualListingConfig extends BaseEchoConfig {
|
||||
}
|
||||
|
||||
private static class ManualListingRegistrar extends AbstractHttpServiceRegistrar {
|
||||
|
||||
public ManualListingRegistrar() {
|
||||
setDefaultClientType(ClientType.WEB_CLIENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
|
||||
setDefaultClientType(ClientType.WEB_CLIENT);
|
||||
registry.forGroup("echo").register(EchoA.class, EchoB.class);
|
||||
registry.forGroup("greeting").register(GreetingA.class, GreetingB.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(ManualDetectionRegistrar.class)
|
||||
private static class ManualDetectionConfig extends BaseEchoConfig {
|
||||
}
|
||||
|
||||
private static class ManualDetectionRegistrar extends AbstractHttpServiceRegistrar {
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
|
||||
setDefaultClientType(ClientType.WEB_CLIENT);
|
||||
registry.forGroup("echo").detectInBasePackages(EchoA.class);
|
||||
registry.forGroup("greeting").detectInBasePackages(GreetingA.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support.echo;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface EchoA {
|
||||
|
||||
@GetExchange("/echoA")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support.echo;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface EchoB {
|
||||
|
||||
@GetExchange("/echoB")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support.greeting;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface GreetingA {
|
||||
|
||||
@GetExchange("/greetingA")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.reactive.function.client.support.greeting;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
public interface GreetingB {
|
||||
|
||||
@GetExchange("/greetingB")
|
||||
String handle(@RequestParam String input);
|
||||
|
||||
}
|
||||
@@ -49,7 +49,7 @@ import java.util.function.Consumer
|
||||
* @author Sebastien Deleuze
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class KotlinWebClientHttpServiceProxyTests {
|
||||
class KotlinWebClientHttpServiceGroupAdapterServiceProxyTests {
|
||||
|
||||
private lateinit var server: MockWebServer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user