From 8326022cd3fe8e77685080fa9b6fc0efc2343a02 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 20 Sep 2024 15:07:20 +0200 Subject: [PATCH] Auto-configure Spring Interface Clients beans. --- .../DocumentConfigurationProperties.java | 6 + settings.gradle | 1 + .../spring-boot-autoconfigure/build.gradle | 4 + ...stractInterfaceClientsImportRegistrar.java | 171 ++++++++++++++++++ .../InterfaceClientsAdapter.java | 37 ++++ .../QualifiedBeanProvider.java | 84 +++++++++ .../interfaceclients/http/HttpClient.java | 44 +++++ .../http/HttpExchangeAdapterProvider.java | 35 ++++ .../http/HttpInterfaceClientsAdapter.java | 71 ++++++++ ...HttpInterfaceClientsAutoConfiguration.java | 116 ++++++++++++ .../HttpInterfaceClientsBaseProperties.java | 40 ++++ .../HttpInterfaceClientsImportRegistrar.java | 38 ++++ .../http/HttpInterfaceClientsProperties.java | 56 ++++++ .../http/RestClientAdapterProvider.java | 89 +++++++++ .../http/RestTemplateAdapterProvider.java | 93 ++++++++++ .../http/WebClientAdapterProvider.java | 89 +++++++++ .../interfaceclients/http/package-info.java | 20 ++ .../interfaceclients/package-info.java | 20 ++ .../NotReactiveWebApplicationCondition.java | 3 +- ...itional-spring-configuration-metadata.json | 12 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../spring-boot-dependencies/build.gradle | 11 ++ src/checkstyle/checkstyle-suppressions.xml | 1 + 23 files changed, 1041 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsImportRegistrar.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/InterfaceClientsAdapter.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/QualifiedBeanProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpClient.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpExchangeAdapterProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAdapter.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsBaseProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsImportRegistrar.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientAdapterProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateAdapterProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientAdapterProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/package-info.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index a7ac94f60ced..a3605d6e76b9 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -69,6 +69,8 @@ void documentConfigurationProperties() throws IOException { snippets.add("application-properties.server", "Server Properties", this::serverPrefixes); snippets.add("application-properties.security", "Security Properties", this::securityPrefixes); snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes); + snippets.add("application-properties.interfaceclients", "Interface Clients Properties", + this::interfaceClientsPrefixes); snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes); snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes); snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes); @@ -205,6 +207,10 @@ private void rsocketPrefixes(Config prefix) { prefix.accept("spring.rsocket"); } + private void interfaceClientsPrefixes(Config prefix) { + prefix.accept("spring.interfaceclients"); + } + private void actuatorPrefixes(Config prefix) { prefix.accept("management"); prefix.accept("micrometer"); diff --git a/settings.gradle b/settings.gradle index 0eb1c0a52bce..0cddd49e554a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -64,6 +64,7 @@ include "spring-boot-project:spring-boot-actuator-autoconfigure" include "spring-boot-project:spring-boot-docker-compose" include "spring-boot-project:spring-boot-devtools" include "spring-boot-project:spring-boot-docs" +include "spring-boot-project:spring-boot-interface-clients" include "spring-boot-project:spring-boot-test" include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 094b81160da2..631efb2d451c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -23,6 +23,10 @@ configurations.all { dependencies { api(project(":spring-boot-project:spring-boot")) +// TODO: Have added it to be able to use CaseUtils and avoid rewriting the code; +// can remove it and duplicate the required method instead + implementation("org.apache.commons:commons-text") + dockerTestImplementation(project(":spring-boot-project:spring-boot-test")) dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker")) dockerTestImplementation("com.redis:testcontainers-redis") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..d648bf6a0f9a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsImportRegistrar.java @@ -0,0 +1,171 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients; + +import java.lang.annotation.Annotation; +import java.text.Normalizer; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.text.CaseUtils; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +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.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Registers bean definitions for annotated Interface Clients in order to automatically + * instantiate client beans based on those interfaces. + * + * @author Josh Long + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +// TODO: Handle AOT +public abstract class AbstractInterfaceClientsImportRegistrar + implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware { + + private static final String INTERFACE_CLIENT_SUFFIX = "InterfaceClient"; + + private static final String BEAN_NAME_ATTRIBUTE_NAME = "beanName"; + + private static final Log logger = LogFactory.getLog(AbstractInterfaceClientsImportRegistrar.class); + + private Environment environment; + + private ResourceLoader resourceLoader; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, + BeanNameGenerator importBeanNameGenerator) { + Assert.isInstanceOf(ListableBeanFactory.class, registry, + "Registry must be an instance of " + ListableBeanFactory.class.getSimpleName()); + ListableBeanFactory beanFactory = (ListableBeanFactory) registry; + Set candidateComponents = discoverCandidateComponents(beanFactory); + for (BeanDefinition candidateComponent : candidateComponents) { + if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) { + registerInterfaceClient(registry, beanFactory, beanDefinition); + } + } + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + protected abstract Class getAnnotation(); + + protected Set discoverCandidateComponents(ListableBeanFactory beanFactory) { + Set candidateComponents = new HashSet<>(); + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + scanner.addIncludeFilter(new AnnotationTypeFilter(getAnnotation())); + List basePackages = AutoConfigurationPackages.get(beanFactory); + for (String basePackage : basePackages) { + candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); + } + return candidateComponents; + } + + private ClassPathScanningCandidateComponentProvider getScanner() { + return new ClassPathScanningCandidateComponentProvider(false, this.environment) { + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + boolean isCandidate = false; + if (beanDefinition.getMetadata().isIndependent()) { + if (!beanDefinition.getMetadata().isAnnotation()) { + isCandidate = true; + } + } + return isCandidate; + } + }; + } + + private void registerInterfaceClient(BeanDefinitionRegistry registry, ListableBeanFactory beanFactory, + AnnotatedBeanDefinition beanDefinition) { + AnnotationMetadata annotatedBeanMetadata = beanDefinition.getMetadata(); + Assert.isTrue(annotatedBeanMetadata.isInterface(), + getAnnotation().getSimpleName() + "can only be placed on an interface."); + MergedAnnotation annotation = annotatedBeanMetadata.getAnnotations().get(getAnnotation()); + String beanClassName = annotatedBeanMetadata.getClassName(); + // The value of the annotation is the qualifier to look for related beans + // while the default beanName corresponds to the simple class name suffixed with + // `InterfaceClient` + String clientId = annotation.getString(MergedAnnotation.VALUE); + String beanName = !ObjectUtils.isEmpty(annotation.getString(BEAN_NAME_ATTRIBUTE_NAME)) + ? annotation.getString(BEAN_NAME_ATTRIBUTE_NAME) : buildBeanName(clientId); + InterfaceClientsAdapter adapter = beanFactory.getBean(InterfaceClientsAdapter.class); + Class beanClass = toClass(beanClassName); + BeanDefinition definition = BeanDefinitionBuilder + .rootBeanDefinition(ResolvableType.forClass(beanClass), + () -> adapter.createClient(beanFactory, clientId, beanClass)) + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE) + .getBeanDefinition(); + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, beanName, new String[] { clientId }); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + } + + private String buildBeanName(String clientId) { + String normalised = Normalizer.normalize(clientId, Normalizer.Form.NFD); + String camelCased = CaseUtils.toCamelCase(normalised, false, '-', '_'); + return camelCased + INTERFACE_CLIENT_SUFFIX; + } + + private static Class toClass(String beanClassName) { + Class beanClass; + try { + beanClass = Class.forName(beanClassName); + } + catch (ClassNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Class not found for interface client " + beanClassName + ": " + ex.getMessage()); + } + throw new RuntimeException(ex); + } + return beanClass; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/InterfaceClientsAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/InterfaceClientsAdapter.java new file mode 100644 index 000000000000..93650aa6bb1b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/InterfaceClientsAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients; + +import org.springframework.beans.factory.ListableBeanFactory; + +/** + * Creates an Interface Client bean for the specified {@code type} and {@code clientId}. + * + * @author Josh Long + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public interface InterfaceClientsAdapter { + + /** + * Default qualifier for user-provided beans used for creating Interface Clients. + */ + String INTERFACE_CLIENTS_DEFAULT_QUALIFIER = "interfaceClients"; + + T createClient(ListableBeanFactory beanFactory, String clientId, Class type); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/QualifiedBeanProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/QualifiedBeanProvider.java new file mode 100644 index 000000000000..95f3fbdc9e28 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/QualifiedBeanProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Utility class containing methods that allow searching for beans with a specific + * qualifier, falling back to the + * {@link InterfaceClientsAdapter#INTERFACE_CLIENTS_DEFAULT_QUALIFIER} qualifier. + * + * @author Josh Long + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public final class QualifiedBeanProvider { + + private QualifiedBeanProvider() { + throw new UnsupportedOperationException("Do not instantiate utility class"); + } + + private static final Log logger = LogFactory.getLog(QualifiedBeanProvider.class); + + public static T qualifiedBean(ListableBeanFactory beanFactory, Class type, String clientId) { + Map matchingClientBeans = getQualifiedBeansOfType(beanFactory, type, clientId); + if (matchingClientBeans.size() > 1) { + throw new NoUniqueBeanDefinitionException(type, matchingClientBeans.keySet()); + } + if (matchingClientBeans.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("No qualified bean of type " + type + " found for " + clientId); + } + Map matchingDefaultBeans = getQualifiedBeansOfType(beanFactory, type, + org.springframework.boot.autoconfigure.interfaceclients.InterfaceClientsAdapter.INTERFACE_CLIENTS_DEFAULT_QUALIFIER); + if (matchingDefaultBeans.size() > 1) { + throw new NoUniqueBeanDefinitionException(type, matchingDefaultBeans.keySet()); + } + if (matchingDefaultBeans.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("No qualified bean of type " + type + " found for default id"); + } + return null; + } + } + return matchingClientBeans.values().iterator().next(); + } + + private static Map getQualifiedBeansOfType(ListableBeanFactory beanFactory, Class type, + String clientId) { + Map beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type); + Map matchingClientBeans = new HashMap<>(); + for (String beanName : beansOfType.keySet()) { + Qualifier qualifier = (beanFactory.findAnnotationOnBean(beanName, Qualifier.class)); + if (qualifier != null && clientId.equals(qualifier.value())) { + matchingClientBeans.put(beanName, beanFactory.getBean(beanName, type)); + } + } + return matchingClientBeans; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpClient.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpClient.java new file mode 100644 index 000000000000..acb2018ec577 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpClient.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +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.web.service.annotation.HttpExchange; + +/** + * Annotation to be placed on interfaces containing {@link HttpExchange}-annotated methods + * in order for a client based on that interface to be autoconfigured. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +// TODO: Consider moving over to Framework. +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface HttpClient { + + String value(); + + String beanName() default ""; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpExchangeAdapterProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpExchangeAdapterProvider.java new file mode 100644 index 000000000000..b855bc82eba4 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpExchangeAdapterProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.web.service.invoker.HttpExchangeAdapter; + +/** + * Provides an {@link HttpExchangeAdapter} instance for the {@code clientId} specified in + * the argument. + * + * @author Josh Long + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +@FunctionalInterface +public interface HttpExchangeAdapterProvider { + + HttpExchangeAdapter get(ListableBeanFactory beanFactory, String clientId); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAdapter.java new file mode 100644 index 000000000000..b14a84d170fa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.autoconfigure.interfaceclients.InterfaceClientsAdapter; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * HTTP-specific {@link InterfaceClientsAdapter} implementation. + *

+ * Will attempt to use an {@link HttpServiceProxyFactory} provided by the user to create + * an HTTP Interface Client. Beans qualified with a specific client id or + * {@link InterfaceClientsAdapter#INTERFACE_CLIENTS_DEFAULT_QUALIFIER}) will be used. If + * no user-provided bean is found, one with a default implementation is created. + * + * @author Josh Long + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class HttpInterfaceClientsAdapter implements InterfaceClientsAdapter { + + private static final Log logger = LogFactory.getLog(HttpInterfaceClientsAdapter.class); + + private final HttpExchangeAdapterProvider adapterProvider; + + public HttpInterfaceClientsAdapter(HttpExchangeAdapterProvider adapterProvider) { + this.adapterProvider = adapterProvider; + } + + @Override + public T createClient(ListableBeanFactory beanFactory, String clientId, Class type) { + HttpServiceProxyFactory proxyFactory = proxyFactory(beanFactory, clientId); + + return proxyFactory.createClient(type); + } + + private HttpServiceProxyFactory proxyFactory(ListableBeanFactory beanFactory, String clientId) { + HttpServiceProxyFactory userProvidedProxyFactory = QualifiedBeanProvider.qualifiedBean(beanFactory, + HttpServiceProxyFactory.class, clientId); + if (userProvidedProxyFactory != null) { + return userProvidedProxyFactory; + } + // create an HttpServiceProxyFactory bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating HttpServiceProxyFactory for '" + clientId + "'"); + } + HttpExchangeAdapter adapter = this.adapterProvider.get(beanFactory, clientId); + return HttpServiceProxyFactory.builderFor(adapter).build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAutoConfiguration.java new file mode 100644 index 000000000000..2b0ab1e38b56 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAutoConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.client.NotReactiveWebApplicationCondition; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.client.support.RestTemplateAdapter; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.support.WebClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for HTTP Interface Clients. + *

+ * This will result in the creation of Interface Client beans from + * {@link HttpClient}-annotated interfaces. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +@AutoConfiguration(after = { RestTemplateAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +@Import(HttpInterfaceClientsImportRegistrar.class) +@EnableConfigurationProperties(HttpInterfaceClientsProperties.class) +@ConditionalOnProperty(value = "spring.interfaceclients.enabled", havingValue = "true") +public class HttpInterfaceClientsAutoConfiguration { + + @Bean + HttpInterfaceClientsAdapter httpInterfaceClientAdapter(HttpExchangeAdapterProvider adapterProvider) { + return new HttpInterfaceClientsAdapter(adapterProvider); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ RestClient.class, RestClientAdapter.class, HttpServiceProxyFactory.class }) + @Conditional(NotReactiveWebApplicationCondition.class) + @ConditionalOnProperty(value = "spring.interfaceclients.http.resttemplate.enabled", havingValue = "false", + matchIfMissing = true) + protected static class RestClientInterfaceClientsConfiguration { + + @Bean + @ConditionalOnBean(RestClient.Builder.class) + @ConditionalOnMissingBean + HttpExchangeAdapterProvider restClientAdapterProvider( + ObjectProvider restClientBuilderProvider, + ObjectProvider propertiesProvider) { + return new RestClientAdapterProvider(restClientBuilderProvider, propertiesProvider); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ RestTemplate.class, RestTemplateAdapter.class, HttpServiceProxyFactory.class }) + @Conditional(NotReactiveWebApplicationCondition.class) + @ConditionalOnProperty(value = "spring.interfaceclients.http.resttemplate.enabled", havingValue = "true") + protected static class RestTemplateInterfaceClientsConfiguration { + + @Bean + @ConditionalOnBean(RestTemplateBuilder.class) + @ConditionalOnMissingBean + HttpExchangeAdapterProvider restTemplateAdapterProvider( + ObjectProvider restTemplateBuilderProvider, + ObjectProvider propertiesProvider) { + return new RestTemplateAdapterProvider(restTemplateBuilderProvider, propertiesProvider); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ WebClient.class, WebClientAdapter.class, HttpServiceProxyFactory.class }) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + protected static class WebClientInterfaceClientsConfiguration { + + @Bean + @ConditionalOnBean(WebClient.Builder.class) + @ConditionalOnMissingBean + HttpExchangeAdapterProvider webClientAdapterProvider(ObjectProvider webClientBuilderProvider, + ObjectProvider propertiesProvider) { + return new WebClientAdapterProvider(webClientBuilderProvider, propertiesProvider); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsBaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsBaseProperties.java new file mode 100644 index 000000000000..236fc61c1e67 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsBaseProperties.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +/** + * Properties for HTTP Interface Clients. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class HttpInterfaceClientsBaseProperties { + + /** + * Base url to set in the underlying HTTP client. By default, set to null. + */ + private String baseUrl = null; + + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..dd3ead5c93c8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsImportRegistrar.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import java.lang.annotation.Annotation; + +import org.springframework.boot.autoconfigure.interfaceclients.AbstractInterfaceClientsImportRegistrar; + +/** + * HTTP-specific {@link AbstractInterfaceClientsImportRegistrar} implementation. Registers + * bean definitions for {@code HttpClient} -annotated Interface Clients in order to + * automatically instantiate client beans based on those interfaces. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class HttpInterfaceClientsImportRegistrar extends AbstractInterfaceClientsImportRegistrar { + + @Override + protected Class getAnnotation() { + return HttpClient.class; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsProperties.java new file mode 100644 index 000000000000..4ecbe8a04597 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for HTTP Interface Clients. + *

+ * Allows using per-client properties or default if no client-specific found. Based on LoadBalancerClientsProperties.java + * + * @author Spencer Gibb + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +@ConfigurationProperties("spring.interfaceclients.http") +public class HttpInterfaceClientsProperties extends HttpInterfaceClientsBaseProperties { + + /** + * Client-specific interface client properties. + */ + private final Map clients = new HashMap<>(); + + public Map getClients() { + return this.clients; + } + + public HttpInterfaceClientsBaseProperties getProperties(String clientId) { + if (clientId == null || !this.getClients().containsKey(clientId)) { + // no specific client properties, return default + return this; + } + // because specifics are overlaid on top of defaults, everything in `properties`, + // unless overridden, is in `clientsProperties` + return this.getClients().get(clientId); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientAdapterProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientAdapterProvider.java new file mode 100644 index 000000000000..576ed4c815d7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientAdapterProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.interfaceclients.InterfaceClientsAdapter; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpExchangeAdapter; + +/** + * {@link RestClient}-backed {@link HttpExchangeAdapterProvider} implementation. + *

+ * Will attempt to use a {@link RestClient} or {@link RestClient.Builder} bean provided by + * the user to create a {@link RestClientAdapter}. Beans qualified with a specific client + * id or {@link InterfaceClientsAdapter#INTERFACE_CLIENTS_DEFAULT_QUALIFIER}) will be + * used. If no user-provided bean is found, one with a default implementation is created. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class RestClientAdapterProvider implements HttpExchangeAdapterProvider { + + private static final Log logger = LogFactory.getLog(RestClientAdapterProvider.class); + + private final ObjectProvider restClientBuilderProvider; + + private final ObjectProvider propertiesProvider; + + public RestClientAdapterProvider(ObjectProvider restClientBuilderProvider, + ObjectProvider propertiesProvider) { + this.restClientBuilderProvider = restClientBuilderProvider; + this.propertiesProvider = propertiesProvider; + } + + @Override + public HttpExchangeAdapter get(ListableBeanFactory beanFactory, String clientId) { + HttpInterfaceClientsProperties properties = this.propertiesProvider.getObject(); + String baseUrl = properties.getProperties(clientId).getBaseUrl(); + + RestClient userProvidedRestClient = QualifiedBeanProvider.qualifiedBean(beanFactory, RestClient.class, + clientId); + if (userProvidedRestClient != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedRestClient = userProvidedRestClient.mutate().baseUrl(baseUrl).build(); + } + return RestClientAdapter.create(userProvidedRestClient); + } + + RestClient.Builder userProvidedRestClientBuilder = QualifiedBeanProvider.qualifiedBean(beanFactory, + RestClient.Builder.class, clientId); + if (userProvidedRestClientBuilder != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedRestClientBuilder.baseUrl(baseUrl); + } + return RestClientAdapter.create(userProvidedRestClientBuilder.build()); + } + // create a RestClientAdapter bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating RestClientAdapter for '" + clientId + "'"); + } + RestClient restClient = this.restClientBuilderProvider.getObject().baseUrl(baseUrl).build(); + return RestClientAdapter.create(restClient); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateAdapterProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateAdapterProvider.java new file mode 100644 index 000000000000..81a509cc63ec --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateAdapterProvider.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.interfaceclients.InterfaceClientsAdapter; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.support.RestTemplateAdapter; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.util.DefaultUriBuilderFactory; + +/** + * {@link RestTemplate}-backed {@link HttpExchangeAdapterProvider} implementation. + *

+ * Will attempt to use a {@link RestTemplate} or {@link RestTemplateBuilder} bean provided + * by the user to create a {@link RestTemplateAdapter}. Beans qualified with a specific + * client id or {@link InterfaceClientsAdapter#INTERFACE_CLIENTS_DEFAULT_QUALIFIER}) will + * be used. If no user-provided bean is found, one with a default implementation is + * created. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class RestTemplateAdapterProvider implements HttpExchangeAdapterProvider { + + private static final Log logger = LogFactory.getLog(RestTemplateAdapterProvider.class); + + private final ObjectProvider restTemplateBuilderProvider; + + private final ObjectProvider propertiesProvider; + + public RestTemplateAdapterProvider(ObjectProvider restTemplateBuilderProvider, + ObjectProvider propertiesProvider) { + this.restTemplateBuilderProvider = restTemplateBuilderProvider; + this.propertiesProvider = propertiesProvider; + } + + @Override + public HttpExchangeAdapter get(ListableBeanFactory beanFactory, String clientId) { + HttpInterfaceClientsProperties properties = this.propertiesProvider.getObject(); + String baseUrl = properties.getProperties(clientId).getBaseUrl(); + + RestTemplate userProvidedRestTemplate = QualifiedBeanProvider.qualifiedBean(beanFactory, RestTemplate.class, + clientId); + if (userProvidedRestTemplate != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedRestTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl)); + } + return RestTemplateAdapter.create(userProvidedRestTemplate); + } + + RestTemplateBuilder userProvidedRestTemplateBuilder = QualifiedBeanProvider.qualifiedBean(beanFactory, + RestTemplateBuilder.class, clientId); + if (userProvidedRestTemplateBuilder != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedRestTemplateBuilder.rootUri(baseUrl); + } + return RestTemplateAdapter.create(userProvidedRestTemplateBuilder.build()); + } + + // create a RestTemplateAdapter bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating RestTemplateAdapter for '" + clientId + "'"); + } + RestTemplate restTemplate = this.restTemplateBuilderProvider.getObject().rootUri(baseUrl).build(); + return RestTemplateAdapter.create(restTemplate); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientAdapterProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientAdapterProvider.java new file mode 100644 index 000000000000..b3386fe3d346 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientAdapterProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2024 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.boot.autoconfigure.interfaceclients.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.interfaceclients.InterfaceClientsAdapter; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.support.WebClientAdapter; +import org.springframework.web.service.invoker.HttpExchangeAdapter; + +/** + * {@link WebClient}-backed {@link HttpExchangeAdapterProvider} implementation. + *

+ * Will attempt to use a {@link WebClient} or {@link WebClient.Builder} bean provided by + * the user to create a {@link WebClientAdapter}. Beans qualified with a specific client + * id or {@link InterfaceClientsAdapter#INTERFACE_CLIENTS_DEFAULT_QUALIFIER}) will be + * used. If no user-provided bean is found, one with a default implementation is created. + * + * @author Olga Maciaszek-Sharma + * @since 3.4.0 + */ +public class WebClientAdapterProvider implements HttpExchangeAdapterProvider { + + private static final Log logger = LogFactory.getLog(WebClientAdapterProvider.class); + + private final ObjectProvider builderProvider; + + private final ObjectProvider propertiesProvider; + + public WebClientAdapterProvider(ObjectProvider builderProvider, + ObjectProvider propertiesProvider) { + this.builderProvider = builderProvider; + this.propertiesProvider = propertiesProvider; + } + + @Override + public HttpExchangeAdapter get(ListableBeanFactory beanFactory, String clientId) { + HttpInterfaceClientsProperties properties = this.propertiesProvider.getObject(); + String baseUrl = properties.getProperties(clientId).getBaseUrl(); + + WebClient userProvidedWebClient = QualifiedBeanProvider.qualifiedBean(beanFactory, WebClient.class, clientId); + if (userProvidedWebClient != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedWebClient = userProvidedWebClient.mutate().baseUrl(baseUrl).build(); + } + return WebClientAdapter.create(userProvidedWebClient); + } + + WebClient.Builder userProvidedWebClientBuilder = QualifiedBeanProvider.qualifiedBean(beanFactory, + WebClient.Builder.class, clientId); + if (userProvidedWebClientBuilder != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in the properties. + if (baseUrl != null) { + userProvidedWebClientBuilder.baseUrl(baseUrl); + } + return WebClientAdapter.create(userProvidedWebClientBuilder.build()); + } + + // create a WebClientAdapter bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating WebClientAdapter for '" + clientId + "'"); + } + WebClient webClient = this.builderProvider.getObject().baseUrl(baseUrl).build(); + return WebClientAdapter.create(webClient); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/package-info.java new file mode 100644 index 000000000000..e15d91d9e018 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2024 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. + */ + +/** + * AutoConfiguration for HTTP Spring Interface Clients. + */ +package org.springframework.boot.autoconfigure.interfaceclients.http; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/package-info.java new file mode 100644 index 000000000000..ae3805c10489 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2024 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. + */ + +/** + * AutoConfiguration for Spring Interface Clients. + */ +package org.springframework.boot.autoconfigure.interfaceclients; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java index b45fbcc58f1f..54a04d832a33 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java @@ -25,8 +25,9 @@ * application. * * @author Phillip Webb + * @since 3.2.0 */ -class NotReactiveWebApplicationCondition extends NoneNestedConditions { +public class NotReactiveWebApplicationCondition extends NoneNestedConditions { NotReactiveWebApplicationCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 63dae453db94..be4905952343 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1611,6 +1611,18 @@ "name": "spring.info.git.location", "defaultValue": "classpath:git.properties" }, + { + "name": "spring.interfaceclients.enabled", + "type": "java.lang.Boolean", + "description": "Whether to automatically instantiate client beans from annotated interfaces.", + "defaultValue": true + }, + { + "name": "spring.interfaceclients.http.resttemplate.enabled", + "type": "java.lang.Boolean", + "description": "Whether to use RestTemplate as the underlying HTTP client for interface clients.", + "defaultValue": true + }, { "name": "spring.jackson.constructor-detector", "defaultValue": "default" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 4250c7a355e7..11b5875b7857 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -150,3 +150,4 @@ org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoCon org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration +org.springframework.boot.autoconfigure.interfaceclients.http.HttpInterfaceClientsAutoConfiguration diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a95593d66785..ece9c09b49b9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -256,6 +256,16 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } + library("Commons Text", "1.12.0") { + group("org.apache.commons") { + modules = [ + "commons-text" + ] + } + links { + site("https://commons.apache.org/proper/commons-text") + } + } library("Couchbase Client", "3.7.2") { group("com.couchbase.client") { modules = [ @@ -1759,6 +1769,7 @@ bom { "spring-boot-configuration-processor", "spring-boot-devtools", "spring-boot-docker-compose", + "spring-boot-interface-clients", "spring-boot-jarmode-tools", "spring-boot-loader", "spring-boot-loader-classic", diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index d44eac1e47ed..0c35b5fa21e9 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -3,6 +3,7 @@ "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" "https://checkstyle.org/dtds/suppressions_1_2.dtd"> +