From ad87582f71d93576c1304cc7a7fb113dcdd2ff6d Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 22 Aug 2024 17:43:12 +0200 Subject: [PATCH] Draft interface clients autoconfiguration --- .../DocumentConfigurationProperties.java | 6 + settings.gradle | 1 + .../spring-boot-autoconfigure/build.gradle | 4 + .../AbstractInterfaceClientsFactoryBean.java | 76 +++++++++ ...stractInterfaceClientsImportRegistrar.java | 152 ++++++++++++++++++ .../QualifiedBeanProvider.java | 74 +++++++++ ...stractHttpInterfaceClientsFactoryBean.java | 63 ++++++++ .../interfaceclients/http/HttpClient.java | 37 +++++ ...HttpInterfaceClientsAutoConfiguration.java | 75 +++++++++ .../HttpInterfaceClientsBaseProperties.java | 37 +++++ .../http/HttpInterfaceClientsProperties.java | 52 ++++++ ...RestClientInterfaceClientsFactoryBean.java | 68 ++++++++ ...ClientInterfaceClientsImportRegistrar.java | 38 +++++ ...stTemplateInterfaceClientsFactoryBean.java | 70 ++++++++ ...mplateInterfaceClientsImportRegistrar.java | 38 +++++ .../WebClientInterfaceClientsFactoryBean.java | 68 ++++++++ ...ClientInterfaceClientsImportRegistrar.java | 38 +++++ .../interfaceclients/http/package-info.java | 20 +++ .../interfaceclients/package-info.java | 20 +++ .../rsocket/RSocketClient.java | 37 +++++ ...cketInterfaceClientsAutoConfiguration.java | 42 +++++ ...RSocketInterfaceClientsBaseProperties.java | 68 ++++++++ .../RSocketInterfaceClientsFactoryBean.java | 89 ++++++++++ ...SocketInterfaceClientsImportRegistrar.java | 38 +++++ .../RSocketInterfaceClientsProperties.java | 49 ++++++ .../rsocket/package-info.java | 20 +++ .../NotReactiveWebApplicationCondition.java | 2 +- ...itional-spring-configuration-metadata.json | 12 ++ ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../spring-boot-dependencies/build.gradle | 11 ++ 30 files changed, 1306 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsFactoryBean.java 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/QualifiedBeanProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/AbstractHttpInterfaceClientsFactoryBean.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/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/HttpInterfaceClientsProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsFactoryBean.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsImportRegistrar.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsFactoryBean.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsImportRegistrar.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsFactoryBean.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsImportRegistrar.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 create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketClient.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsBaseProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsFactoryBean.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsImportRegistrar.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/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 fe32fe1063b0..ddf6ebf7ee58 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,6 +65,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 13ad7ba4cbd9..20725a461410 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("org.assertj:assertj-core") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..b1f0f6d6afbb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsFactoryBean.java @@ -0,0 +1,76 @@ +/* + * 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.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.Assert; + +/** + * @author Olga Maciaszek-Sharma + */ +public abstract class AbstractInterfaceClientsFactoryBean implements FactoryBean, ApplicationContextAware { + + protected Class type; + + protected String beanName; + + protected String clientId; + + protected ConfigurableApplicationContext applicationContext; + + @Override + public Class getObjectType() { + return this.type; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext, + "ApplicationContext must be an instance of " + ConfigurableApplicationContext.class.getSimpleName()); + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + + } + + public Class getType() { + return this.type; + } + + public void setType(Class type) { + this.type = type; + } + + public String getBeanName() { + return this.beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + +} 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..e12454771346 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/AbstractInterfaceClientsImportRegistrar.java @@ -0,0 +1,152 @@ +/* + * 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.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.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; + +/** + * @author Josh Long + * @author Olga Maciaszek-Sharma + */ +// 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 String CLIENT_ID_ATTRIBUTE_NAME = "clientId"; + + private static final String BEAN_CLASS_ATTRIBUTE_NAME = "type"; + + 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, beanDefinition); + } + } + } + + private void registerInterfaceClient(BeanDefinitionRegistry registry, 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); + BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(getFactoryBeanClass()); + definitionBuilder.addPropertyValue(BEAN_NAME_ATTRIBUTE_NAME, beanName); + definitionBuilder.addPropertyValue(CLIENT_ID_ATTRIBUTE_NAME, clientId); + definitionBuilder.addPropertyValue(BEAN_CLASS_ATTRIBUTE_NAME, beanClassName); + definitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + AbstractBeanDefinition definition = definitionBuilder.getBeanDefinition(); + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, beanName, new String[] { clientId }); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + } + + 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; + } + }; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + protected abstract Class getAnnotation(); + + protected abstract Class getFactoryBeanClass(); + + private String buildBeanName(String clientId) { + String normalised = Normalizer.normalize(clientId, Normalizer.Form.NFD); + String camelCased = CaseUtils.toCamelCase(normalised, false, '-', '_'); + return camelCased + INTERFACE_CLIENT_SUFFIX; + } + +} 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..07edb12c012d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/QualifiedBeanProvider.java @@ -0,0 +1,74 @@ +/* + * 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.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +/** + * @author Josh Long + * @author Olga Maciaszek-Sharma + */ +public final class QualifiedBeanProvider { + + private static final Log logger = LogFactory.getLog(QualifiedBeanProvider.class); + + public static T qualifiedBean(ConfigurableListableBeanFactory 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, clientId); + 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(ConfigurableListableBeanFactory 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/AbstractHttpInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/AbstractHttpInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..b3d6b5f4adbf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/AbstractHttpInterfaceClientsFactoryBean.java @@ -0,0 +1,63 @@ +/* + * 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.boot.autoconfigure.interfaceclients.AbstractInterfaceClientsFactoryBean; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * @author Olga Maciaszek-Sharma + */ +public abstract class AbstractHttpInterfaceClientsFactoryBean extends AbstractInterfaceClientsFactoryBean { + + private static final Log logger = LogFactory.getLog(AbstractHttpInterfaceClientsFactoryBean.class); + + @Override + public Object getObject() throws Exception { + HttpServiceProxyFactory proxyFactory = proxyFactory(); + return proxyFactory.createClient(this.type); + } + + protected String getBaseUrl() { + HttpInterfaceClientsProperties properties = this.applicationContext + .getBean(HttpInterfaceClientsProperties.class); + + return properties.getProperties(this.clientId).getBaseUrl(); + } + + protected abstract HttpExchangeAdapter exchangeAdapter(); + + private HttpServiceProxyFactory proxyFactory() { + HttpServiceProxyFactory userProvidedProxyFactory = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), HttpServiceProxyFactory.class, this.clientId); + if (userProvidedProxyFactory != null) { + return userProvidedProxyFactory; + } + // create an HttpServiceProxyFactory bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating HttpServiceProxyFactory for '" + this.clientId + "'"); + } + HttpExchangeAdapter adapter = exchangeAdapter(); + return HttpServiceProxyFactory.builderFor(adapter).build(); + } + +} 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..10e717ec2a29 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpClient.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.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; + +/** + * @author Olga Maciaszek-Sharma + */ +@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/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..cee8c7b85fad --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsAutoConfiguration.java @@ -0,0 +1,75 @@ +/* + * 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.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +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.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; + +/** + * @author Olga Maciaszek-Sharma + */ +@AutoConfiguration(after = { RestTemplateAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +@EnableConfigurationProperties(HttpInterfaceClientsProperties.class) +@ConditionalOnProperty(value = "spring.interfaceclients.enabled", havingValue = "true") +public class HttpInterfaceClientsAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ RestClient.class, RestClientAdapter.class, HttpServiceProxyFactory.class }) + @Conditional(NotReactiveWebApplicationCondition.class) + @ConditionalOnProperty(value = "spring.interfaceclients.http.resttemplate.enabled", havingValue = "false", + matchIfMissing = true) + @Import(RestClientInterfaceClientsImportRegistrar.class) + protected static class RestClientAdapterProviderConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ RestTemplate.class, RestTemplateAdapter.class, HttpServiceProxyFactory.class }) + @Conditional(NotReactiveWebApplicationCondition.class) + @ConditionalOnProperty(value = "spring.interfaceclients.http.resttemplate.enabled", havingValue = "true") + @Import(RestTemplateInterfaceClientsImportRegistrar.class) + protected static class RestTemplateAdapterProviderConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ WebClient.class, WebClientAdapter.class, HttpServiceProxyFactory.class }) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + @Import(WebClientInterfaceClientsImportRegistrar.class) + protected static class WebClientAdapterProviderConfiguration { + + } + +} 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..06d1250163b1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsBaseProperties.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.http; + +/** + * @author Olga Maciaszek-Sharma + */ +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/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..5367dac53a40 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/HttpInterfaceClientsProperties.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Use per-client properties or default if no client-specific found. Based on LoadBalancerClientsProperties.java + * + * @author Olga Maciaszek-Sharma + */ +@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/RestClientInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..cc0a3cefd088 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsFactoryBean.java @@ -0,0 +1,68 @@ +/* + * 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.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpExchangeAdapter; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RestClientInterfaceClientsFactoryBean extends AbstractHttpInterfaceClientsFactoryBean { + + private static final Log logger = LogFactory.getLog(RestClientInterfaceClientsFactoryBean.class); + + @Override + protected HttpExchangeAdapter exchangeAdapter() { + String baseUrl = getBaseUrl(); + + RestClient userProvidedRestClient = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RestClient.class, this.clientId); + if (userProvidedRestClient != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in properties. + if (baseUrl != null) { + userProvidedRestClient = userProvidedRestClient.mutate().baseUrl(baseUrl).build(); + } + return RestClientAdapter.create(userProvidedRestClient); + } + + RestClient.Builder userProvidedRestClientBuilder = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RestClient.Builder.class, this.clientId); + if (userProvidedRestClientBuilder != null) { + + 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 '" + this.clientId + "'"); + } + RestClient.Builder builder = this.applicationContext.getBean(RestClient.Builder.class); + RestClient restClient = builder.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/RestClientInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..f4b0fc852e53 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestClientInterfaceClientsImportRegistrar.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; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RestClientInterfaceClientsImportRegistrar extends AbstractInterfaceClientsImportRegistrar { + + @Override + protected Class getAnnotation() { + return HttpClient.class; + } + + @Override + protected Class getFactoryBeanClass() { + return RestClientInterfaceClientsFactoryBean.class; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..b6619da6a60c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsFactoryBean.java @@ -0,0 +1,70 @@ +/* + * 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.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; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RestTemplateInterfaceClientsFactoryBean extends AbstractHttpInterfaceClientsFactoryBean { + + private static final Log logger = LogFactory.getLog(RestTemplateInterfaceClientsFactoryBean.class); + + @Override + protected HttpExchangeAdapter exchangeAdapter() { + String baseUrl = getBaseUrl(); + + RestTemplate userProvidedRestTemplate = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RestTemplate.class, this.clientId); + if (userProvidedRestTemplate != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in properties. + if (baseUrl != null) { + userProvidedRestTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl)); + } + return RestTemplateAdapter.create(userProvidedRestTemplate); + } + + RestTemplateBuilder userProvidedRestTemplateBuilder = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RestTemplateBuilder.class, this.clientId); + if (userProvidedRestTemplateBuilder != null) { + + 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 '" + this.clientId + "'"); + } + RestTemplateBuilder builder = this.applicationContext.getBean(RestTemplateBuilder.class); + RestTemplate restTemplate = builder.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/RestTemplateInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..99ce55762d17 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/RestTemplateInterfaceClientsImportRegistrar.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; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RestTemplateInterfaceClientsImportRegistrar extends AbstractInterfaceClientsImportRegistrar { + + @Override + protected Class getAnnotation() { + return HttpClient.class; + } + + @Override + protected Class getFactoryBeanClass() { + return RestTemplateInterfaceClientsFactoryBean.class; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..d9accc1ca86a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsFactoryBean.java @@ -0,0 +1,68 @@ +/* + * 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.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; + +/** + * @author Olga Maciaszek-Sharma + */ +public class WebClientInterfaceClientsFactoryBean extends AbstractHttpInterfaceClientsFactoryBean { + + private static final Log logger = LogFactory.getLog(WebClientInterfaceClientsFactoryBean.class); + + @Override + protected HttpExchangeAdapter exchangeAdapter() { + String baseUrl = getBaseUrl(); + + WebClient userProvidedWebClient = QualifiedBeanProvider.qualifiedBean(this.applicationContext.getBeanFactory(), + WebClient.class, this.clientId); + if (userProvidedWebClient != null) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in properties. + if (baseUrl != null) { + userProvidedWebClient = userProvidedWebClient.mutate().baseUrl(baseUrl).build(); + } + return WebClientAdapter.create(userProvidedWebClient); + } + + WebClient.Builder userProvidedWebClientBuilder = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), WebClient.Builder.class, this.clientId); + if (userProvidedWebClientBuilder != null) { + + 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 '" + this.clientId + "'"); + } + WebClient.Builder builder = this.applicationContext.getBean(WebClient.Builder.class); + WebClient webClient = builder.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/WebClientInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..0ee05da3d9b1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/http/WebClientInterfaceClientsImportRegistrar.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; + +/** + * @author Olga Maciaszek-Sharma + */ +public class WebClientInterfaceClientsImportRegistrar extends AbstractInterfaceClientsImportRegistrar { + + @Override + protected Class getAnnotation() { + return HttpClient.class; + } + + @Override + protected Class getFactoryBeanClass() { + return WebClientInterfaceClientsFactoryBean.class; + } + +} 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/interfaceclients/rsocket/RSocketClient.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketClient.java new file mode 100644 index 000000000000..a46a6eaf3e07 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketClient.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.rsocket; + +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; + +/** + * @author Olga Maciaszek-Sharma + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RSocketClient { + + String value(); + + String beanName() default ""; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsAutoConfiguration.java new file mode 100644 index 000000000000..ab461434ae43 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.rsocket; + +import io.rsocket.transport.netty.server.TcpServerTransport; +import reactor.netty.http.server.HttpServer; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.service.RSocketServiceProxyFactory; + +/** + * @author Olga Maciaszek-Sharma + */ +@AutoConfiguration(after = RSocketRequesterAutoConfiguration.class) +@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, HttpServer.class, TcpServerTransport.class, + RSocketServiceProxyFactory.class }) +@EnableConfigurationProperties(RSocketInterfaceClientsProperties.class) +@ConditionalOnProperty(value = "spring.interfaceclients.enabled", havingValue = "true") +@Import(RSocketInterfaceClientsImportRegistrar.class) +public class RSocketInterfaceClientsAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsBaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsBaseProperties.java new file mode 100644 index 000000000000..d89b8b91b669 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsBaseProperties.java @@ -0,0 +1,68 @@ +/* + * 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.rsocket; + +import java.net.URI; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RSocketInterfaceClientsBaseProperties { + + /** + * Host used to create a TCP connection for RSocketRequester. By default, set to null. + * Both host and port are required for a TCP connection. + */ + private String host; + + /** + * Port used to create a TCP connection for RSocketRequester. By default, set to null. + * Both host and port are required for a TCP connection. + */ + private Integer port; + + /** + * URI used to create a Websocket connection for RSocketRequester. By default, set to + * null. + */ + private URI uri; + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return this.port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public URI getUri() { + return this.uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsFactoryBean.java new file mode 100644 index 000000000000..8e5f040d8593 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsFactoryBean.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.rsocket; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.interfaceclients.AbstractInterfaceClientsFactoryBean; +import org.springframework.boot.autoconfigure.interfaceclients.QualifiedBeanProvider; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.service.RSocketServiceProxyFactory; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RSocketInterfaceClientsFactoryBean extends AbstractInterfaceClientsFactoryBean { + + private static final Log logger = LogFactory.getLog(RSocketInterfaceClientsFactoryBean.class); + + @Override + public Object getObject() throws Exception { + RSocketServiceProxyFactory factory = proxyFactory(); + return factory.createClient(this.type); + } + + private RSocketServiceProxyFactory proxyFactory() { + RSocketServiceProxyFactory userProvidedProxyFactory = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RSocketServiceProxyFactory.class, this.clientId); + if (userProvidedProxyFactory != null) { + return userProvidedProxyFactory; + } + // create an RSocketServiceProxyFactory bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating RSocketServiceProxyFactory for '" + this.clientId + "'"); + } + RSocketRequester requester = rsocketRequester(); + return RSocketServiceProxyFactory.builder(requester).build(); + } + + private RSocketRequester rsocketRequester() { + RSocketRequester userProvidedRequester = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RSocketRequester.class, this.clientId); + if (userProvidedRequester != null) { + return userProvidedRequester; + } + + RSocketRequester.Builder userProvidedRSocketRequesterBuilder = QualifiedBeanProvider + .qualifiedBean(this.applicationContext.getBeanFactory(), RSocketRequester.Builder.class, this.clientId); + if (userProvidedRSocketRequesterBuilder != null) { + return toRequester(userProvidedRSocketRequesterBuilder); + } + + // create an RSocketRequester bean with default implementation + if (logger.isDebugEnabled()) { + logger.debug("Creating RSocketRequester for '" + this.clientId + "'"); + } + RSocketRequester.Builder builder = this.applicationContext.getBean(RSocketRequester.Builder.class); + return toRequester(builder); + } + + private RSocketRequester toRequester(RSocketRequester.Builder requesterBuilder) { + // If the user wants to set the baseUrl directly on the builder, + // it should not be set in properties. + RSocketInterfaceClientsProperties properties = this.applicationContext + .getBean(RSocketInterfaceClientsProperties.class); + if (properties.getHost() != null && properties.getPort() != null) { + return requesterBuilder.tcp(properties.getHost(), properties.getPort()); + } + if (properties.getUri() != null) { + return requesterBuilder.websocket(properties.getUri()); + } + throw new IllegalArgumentException("No Host and Port or URI provided in RSocketInterfaceClientsProperties"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsImportRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsImportRegistrar.java new file mode 100644 index 000000000000..7e4e94a9c64e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsImportRegistrar.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.rsocket; + +import java.lang.annotation.Annotation; + +import org.springframework.boot.autoconfigure.interfaceclients.AbstractInterfaceClientsImportRegistrar; + +/** + * @author Olga Maciaszek-Sharma + */ +public class RSocketInterfaceClientsImportRegistrar extends AbstractInterfaceClientsImportRegistrar { + + @Override + protected Class getAnnotation() { + return RSocketClient.class; + } + + @Override + protected Class getFactoryBeanClass() { + return RSocketInterfaceClientsFactoryBean.class; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsProperties.java new file mode 100644 index 000000000000..733c334d91d6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/RSocketInterfaceClientsProperties.java @@ -0,0 +1,49 @@ +/* + * 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.rsocket; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Olga Maciaszek-Sharma + */ +@ConfigurationProperties("spring.interfaceclients.rsocket") +public class RSocketInterfaceClientsProperties extends RSocketInterfaceClientsBaseProperties { + + /** + * Client-specific interface client properties. + */ + private final Map clients = new HashMap<>(); + + public Map getClients() { + return this.clients; + } + + public RSocketInterfaceClientsBaseProperties 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/rsocket/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/package-info.java new file mode 100644 index 000000000000..79c59c5b5b2d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/interfaceclients/rsocket/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 RSocket Spring Interface Clients. + */ +package org.springframework.boot.autoconfigure.interfaceclients.rsocket; 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..450fbd9eebe5 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 @@ -26,7 +26,7 @@ * * @author Phillip Webb */ -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..9744ee8c50af 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,5 @@ 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 +org.springframework.boot.autoconfigure.interfaceclients.rsocket.RSocketInterfaceClientsAutoConfiguration diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 564d87a4b872..1cd48a9b45b7 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 = [ @@ -1753,6 +1763,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",