Skip to content

Commit 7a8b337

Browse files
committed
Add @HttpServiceClient scanning auto-configuration
Refactor `HttpServiceClientAutoConfiguration` and `ReactiveHttpServiceClientAutoConfiguration` to support scanning for `@HttpServiceClient` annotated interfaces. Closes gh-46782
1 parent 11c5a8c commit 7a8b337

File tree

14 files changed

+600
-88
lines changed

14 files changed

+600
-88
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.http.client.autoconfigure.service;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.context.annotation.Conditional;
26+
27+
/**
28+
* {@link Conditional @Conditional} that matches when one or more HTTP Service bean has
29+
* been registered.
30+
*
31+
* @author Phillip Webb@
32+
* @since 4.0.0
33+
*/
34+
@Target({ ElementType.TYPE, ElementType.METHOD })
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Documented
37+
@Conditional(OnMissingHttpServiceProxyBeanCondition.class)
38+
public @interface ConditionalOnMissingHttpServiceProxyBean {
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.http.client.autoconfigure.service;
18+
19+
import org.springframework.beans.factory.BeanFactory;
20+
import org.springframework.beans.factory.HierarchicalBeanFactory;
21+
import org.springframework.beans.factory.config.BeanDefinition;
22+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
23+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
24+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava;
26+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
27+
import org.springframework.context.annotation.Condition;
28+
import org.springframework.context.annotation.ConditionContext;
29+
import org.springframework.context.annotation.ConfigurationCondition;
30+
import org.springframework.core.type.AnnotatedTypeMetadata;
31+
32+
/**
33+
* {@link Condition} that checks for any HTTP Service proxy bean.
34+
*
35+
* @author Phillip Webb
36+
* @see ConditionalOnJava
37+
*/
38+
class OnMissingHttpServiceProxyBeanCondition extends SpringBootCondition implements ConfigurationCondition {
39+
40+
static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName";
41+
42+
@Override
43+
public ConfigurationPhase getConfigurationPhase() {
44+
return ConfigurationPhase.REGISTER_BEAN;
45+
}
46+
47+
@Override
48+
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
49+
ConditionMessage.Builder message = ConditionMessage
50+
.forCondition(ConditionalOnMissingHttpServiceProxyBean.class);
51+
BeanFactory beanFactory = context.getBeanFactory();
52+
while (beanFactory != null) {
53+
if (beanFactory instanceof ConfigurableListableBeanFactory configurableListableBeanFactory
54+
&& hasHttpServiceProxyBeanDefinition(configurableListableBeanFactory)) {
55+
return ConditionOutcome.noMatch(message.foundExactly("HTTP Service proxy bean"));
56+
}
57+
beanFactory = (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory)
58+
? hierarchicalBeanFactory.getParentBeanFactory() : null;
59+
}
60+
return ConditionOutcome.match(message.didNotFind("").items("HTTP Service proxy beans"));
61+
}
62+
63+
private boolean hasHttpServiceProxyBeanDefinition(ConfigurableListableBeanFactory beanFactory) {
64+
for (String beanName : beanFactory.getBeanDefinitionNames()) {
65+
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
66+
if (beanDefinition.hasAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE)) {
67+
return true;
68+
}
69+
}
70+
return false;
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Common support code for HTTP Service Clients.
19+
*/
20+
@NullMarked
21+
package org.springframework.boot.http.client.autoconfigure.service;
22+
23+
import org.jspecify.annotations.NullMarked;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.http.client.autoconfigure.service;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.test.util.ReflectionTestUtils;
25+
import org.springframework.web.service.annotation.GetExchange;
26+
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
27+
import org.springframework.web.service.registry.ImportHttpServices;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Tests for
33+
* {@link ConditionalOnMissingHttpServiceProxyBean @ConditionalOnMissingHttpServiceProxyBean}.
34+
*
35+
* @author Phillip Webb
36+
*/
37+
class ConditionalOnMissingHttpServiceProxyBeanTests {
38+
39+
@Test
40+
void attributeNameMatchesSpringFramework() {
41+
assertThat(OnMissingHttpServiceProxyBeanCondition.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE).isEqualTo(
42+
ReflectionTestUtils.getField(AbstractHttpServiceRegistrar.class, "HTTP_SERVICE_GROUP_NAME_ATTRIBUTE"));
43+
}
44+
45+
@Test
46+
void getOutcomeWhenNoHttpServiceProxyMatches() {
47+
new ApplicationContextRunner().withUserConfiguration(TestConfiguration.class)
48+
.run((context) -> assertThat(context).hasBean("test"));
49+
}
50+
51+
@Test
52+
void getOutcomeWhenHasHttpServiceProxyDoesNotMatch() {
53+
new ApplicationContextRunner()
54+
.withUserConfiguration(HttpServiceProxyConfiguration.class, TestConfiguration.class)
55+
.run((context) -> assertThat(context).hasSingleBean(TestHttpService.class).doesNotHaveBean("test"));
56+
}
57+
58+
@Configuration(proxyBeanMethods = false)
59+
@ImportHttpServices(TestHttpService.class)
60+
static class HttpServiceProxyConfiguration {
61+
62+
}
63+
64+
@Configuration(proxyBeanMethods = false)
65+
static class TestConfiguration {
66+
67+
@Bean
68+
@ConditionalOnMissingHttpServiceProxyBean
69+
String test() {
70+
return "test";
71+
}
72+
73+
}
74+
75+
interface TestHttpService {
76+
77+
@GetExchange("/test")
78+
String test();
79+
80+
}
81+
82+
}

module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,14 @@
1616

1717
package org.springframework.boot.restclient.autoconfigure.service;
1818

19-
import org.springframework.beans.factory.BeanClassLoaderAware;
20-
import org.springframework.beans.factory.ObjectProvider;
2119
import org.springframework.boot.autoconfigure.AutoConfiguration;
22-
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2320
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2421
import org.springframework.boot.context.properties.EnableConfigurationProperties;
25-
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
26-
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
2722
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
28-
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
29-
import org.springframework.boot.restclient.RestClientCustomizer;
3023
import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
31-
import org.springframework.boot.ssl.SslBundles;
32-
import org.springframework.context.annotation.Bean;
3324
import org.springframework.context.annotation.Conditional;
34-
import org.springframework.web.client.ApiVersionFormatter;
35-
import org.springframework.web.client.ApiVersionInserter;
25+
import org.springframework.context.annotation.Import;
3626
import org.springframework.web.client.support.RestClientAdapter;
37-
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
3827
import org.springframework.web.service.registry.ImportHttpServices;
3928

4029
/**
@@ -50,39 +39,9 @@
5039
*/
5140
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class })
5241
@ConditionalOnClass(RestClientAdapter.class)
53-
@ConditionalOnBean(HttpServiceProxyRegistry.class)
5442
@Conditional(NotReactiveWebApplicationCondition.class)
5543
@EnableConfigurationProperties(HttpClientServiceProperties.class)
56-
public final class HttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
57-
58-
@SuppressWarnings("NullAway.Init")
59-
private ClassLoader beanClassLoader;
60-
61-
HttpServiceClientAutoConfiguration() {
62-
}
63-
64-
@Override
65-
public void setBeanClassLoader(ClassLoader classLoader) {
66-
this.beanClassLoader = classLoader;
67-
}
68-
69-
@Bean
70-
RestClientPropertiesHttpServiceGroupConfigurer restClientPropertiesHttpServiceGroupConfigurer(
71-
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientProperties> httpClientProperties,
72-
HttpClientServiceProperties serviceProperties,
73-
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
74-
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
75-
ObjectProvider<ApiVersionInserter> apiVersionInserter,
76-
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
77-
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
78-
httpClientProperties.getIfAvailable(), serviceProperties, clientFactoryBuilder,
79-
clientHttpRequestFactorySettings, apiVersionInserter, apiVersionFormatter);
80-
}
81-
82-
@Bean
83-
RestClientCustomizerHttpServiceGroupConfigurer restClientCustomizerHttpServiceGroupConfigurer(
84-
ObjectProvider<RestClientCustomizer> customizers) {
85-
return new RestClientCustomizerHttpServiceGroupConfigurer(customizers);
86-
}
44+
@Import({ ImportHttpServiceClientsConfiguration.class, RestClientHttpServiceClientConfiguration.class })
45+
public final class HttpServiceClientAutoConfiguration {
8746

8847
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.restclient.autoconfigure.service;
18+
19+
import org.springframework.beans.factory.BeanFactory;
20+
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
21+
import org.springframework.boot.http.client.autoconfigure.service.ConditionalOnMissingHttpServiceProxyBean;
22+
import org.springframework.boot.restclient.autoconfigure.service.ImportHttpServiceClientsConfiguration.ImportHttpServiceClients;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.context.annotation.Import;
25+
import org.springframework.core.type.AnnotationMetadata;
26+
import org.springframework.web.service.registry.AbstractClientHttpServiceRegistrar;
27+
import org.springframework.web.service.registry.HttpServiceClient;
28+
29+
/**
30+
* {@link Configuration @Configuration} to import {@link ImportHttpServiceClients} when no
31+
* user-defined HTTP service client beans are found.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
@Configuration(proxyBeanMethods = false)
36+
@ConditionalOnMissingHttpServiceProxyBean
37+
@Import(ImportHttpServiceClients.class)
38+
class ImportHttpServiceClientsConfiguration {
39+
40+
/**
41+
* {@link AbstractClientHttpServiceRegistrar} to import
42+
* {@link HttpServiceClient @HttpServiceClient} annotated classes from
43+
* {@link AutoConfigurationPackages}.
44+
*/
45+
static class ImportHttpServiceClients extends AbstractClientHttpServiceRegistrar {
46+
47+
private final BeanFactory beanFactory;
48+
49+
ImportHttpServiceClients(BeanFactory beanFactory) {
50+
this.beanFactory = beanFactory;
51+
}
52+
53+
@Override
54+
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
55+
if (AutoConfigurationPackages.has(this.beanFactory)) {
56+
findAndRegisterHttpServiceClients(registry, AutoConfigurationPackages.get(this.beanFactory));
57+
}
58+
}
59+
60+
}
61+
62+
}

0 commit comments

Comments
 (0)