diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc3f0a0d..5fe69c22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,11 +3,13 @@ on: push: branches: - main + - 3.3.x - 3.2.x - 3.1.x pull_request: branches: - main + - 3.3.x - 3.2.x - 3.1.x jobs: diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index fbc0d1e3..354a813e 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 3.3.x - 3.2.x - 3.1.x jobs: diff --git a/build.gradle b/build.gradle index 2cc1ea83..cae98a70 100644 --- a/build.gradle +++ b/build.gradle @@ -60,9 +60,16 @@ allprojects { } test { + systemProperties("spring.cloud.compatibility-verifier.enabled": "false") useJUnitPlatform() } + afterEvaluate { + tasks.findByName("bootRun")?.configure { + systemProperty("spring.cloud.compatibility-verifier.enabled", "false") + } + } + apply plugin: "com.diffplug.spotless" spotless { encoding "UTF-8" diff --git a/examples/loadbalancer/build.gradle b/examples/loadbalancer/build.gradle index 1db11779..efe4f44c 100644 --- a/examples/loadbalancer/build.gradle +++ b/examples/loadbalancer/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "org.springframework.boot" +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation(project(":starters:httpexchange-spring-boot-starter")) diff --git a/examples/minimal/build.gradle b/examples/minimal/build.gradle index cd93e5d9..a36e9217 100644 --- a/examples/minimal/build.gradle +++ b/examples/minimal/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "org.springframework.boot" +} + dependencies { implementation(project(":starters:httpexchange-spring-boot-starter")) diff --git a/examples/quick-start/build.gradle b/examples/quick-start/build.gradle index f4b3d2ce..aabea0c8 100644 --- a/examples/quick-start/build.gradle +++ b/examples/quick-start/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "org.springframework.boot" +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") diff --git a/examples/reactive/build.gradle b/examples/reactive/build.gradle index 18389c3e..afeaa2d1 100644 --- a/examples/reactive/build.gradle +++ b/examples/reactive/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "org.springframework.boot" +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") implementation(project(":starters:httpexchange-spring-boot-starter")) diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ConfigurerCopier.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ConfigurerCopier.java new file mode 100644 index 00000000..51e1bf46 --- /dev/null +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ConfigurerCopier.java @@ -0,0 +1,75 @@ +package io.github.danielliu1123.httpexchange; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.util.ReflectionUtils; + +/** + * @author Freeman + */ +final class ConfigurerCopier { + + private static final Map restClientBuilderConfigurerProperties; + private static final Map restTemplateBuilderConfigurerProperties; + + static { + restClientBuilderConfigurerProperties = Arrays.stream(RestClientBuilderConfigurer.class.getDeclaredFields()) + .peek(ReflectionUtils::makeAccessible) + .collect(Collectors.toMap(Field::getName, Function.identity())); + restTemplateBuilderConfigurerProperties = Arrays.stream(RestTemplateBuilderConfigurer.class.getDeclaredFields()) + .peek(ReflectionUtils::makeAccessible) + .collect(Collectors.toMap(Field::getName, Function.identity())); + } + + public static RestClientBuilderConfigurer copyRestClientBuilderConfigurer(RestClientBuilderConfigurer source) { + + var target = new RestClientBuilderConfigurer(); + + for (var entry : restClientBuilderConfigurerProperties.entrySet()) { + var field = entry.getValue(); + var value = ReflectionUtils.getField(field, source); + if (value != null) { + ReflectionUtils.setField(field, target, value); + } + } + + return target; + } + + public static void setRestClientBuilderConfigurerProperty( + RestClientBuilderConfigurer target, String name, Object value) { + var field = restClientBuilderConfigurerProperties.get(name); + if (field != null) { + ReflectionUtils.setField(field, target, value); + } + } + + public static RestTemplateBuilderConfigurer copyRestTemplateBuilderConfigurer( + RestTemplateBuilderConfigurer source) { + + var target = new RestTemplateBuilderConfigurer(); + + for (var entry : restTemplateBuilderConfigurerProperties.entrySet()) { + var field = entry.getValue(); + var value = ReflectionUtils.getField(field, source); + if (value != null) { + ReflectionUtils.setField(field, target, value); + } + } + + return target; + } + + public static void setRestTemplateBuilderConfigurerProperty( + RestTemplateBuilderConfigurer target, String name, Object value) { + var field = restTemplateBuilderConfigurerProperties.get(name); + if (field != null) { + ReflectionUtils.setField(field, target, value); + } + } +} diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ExchangeClientCreator.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ExchangeClientCreator.java index d5351b85..ec52bc9e 100644 --- a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ExchangeClientCreator.java +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/ExchangeClientCreator.java @@ -9,6 +9,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.github.danielliu1123.httpexchange.shaded.ShadedHttpServiceProxyFactory; +import jakarta.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.time.Duration; @@ -24,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.SpringBootVersion; import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer; import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; import org.springframework.boot.ssl.SslBundle; @@ -218,11 +220,9 @@ private void setEmbeddedValueResolver(HttpServiceProxyFactory.Builder builder) { private RestTemplate buildRestTemplate(HttpExchangeProperties.Channel channelConfig) { RestTemplateBuilder builder = new RestTemplateBuilder(); - RestTemplateBuilderConfigurer configurer = - beanFactory.getBeanProvider(RestTemplateBuilderConfigurer.class).getIfUnique(); - if (configurer != null) { - builder = configurer.configure(builder); - } + + builder = configureRestTemplateBuilder(builder, channelConfig); + if (StringUtils.hasText(channelConfig.getBaseUrl())) { builder = builder.rootUri(getRealBaseUrl(channelConfig)); } @@ -234,7 +234,10 @@ private RestTemplate buildRestTemplate(HttpExchangeProperties.Channel channelCon } // Set default request factory - builder = builder.requestFactory(() -> getRequestFactory(channelConfig)); + // No need to do this when Spring Boot version >= 3.4.0 + if (isSpringBootVersionLessThan340()) { + builder = builder.requestFactory(() -> getRequestFactory(channelConfig)); + } if (isLoadBalancerEnabled(channelConfig)) { Set lbInterceptors = new LinkedHashSet<>(); @@ -255,7 +258,9 @@ private RestTemplate buildRestTemplate(HttpExchangeProperties.Channel channelCon restTemplate.setInterceptors( restTemplate.getInterceptors().stream().distinct().toList()); - setTimeoutByConfig(restTemplate.getRequestFactory(), channelConfig); + if (isSpringBootVersionLessThan340()) { + setTimeoutByConfig(restTemplate.getRequestFactory(), channelConfig); + } beanFactory .getBeanProvider(HttpClientCustomizer.RestTemplateCustomizer.class) @@ -280,10 +285,12 @@ private WebClient buildWebClient(HttpExchangeProperties.Channel channelConfig) { .forEach(header -> builder.defaultHeader( header.getKey(), header.getValues().toArray(String[]::new))); } - if (channelConfig.getReadTimeout() != null) { - builder.filter((request, next) -> - next.exchange(request).timeout(Duration.ofMillis(channelConfig.getReadTimeout()))); + + var readTimeout = getReadTimeout(channelConfig); + if (readTimeout != null) { + builder.filter((request, next) -> next.exchange(request).timeout(readTimeout)); } + if (isLoadBalancerEnabled(channelConfig)) { builder.filters(filters -> { Set allFilters = new LinkedHashSet<>(filters); @@ -306,12 +313,33 @@ private WebClient buildWebClient(HttpExchangeProperties.Channel channelConfig) { return builder.build(); } + @Nullable + private Duration getReadTimeout(HttpExchangeProperties.Channel channelConfig) { + var duration = Optional.ofNullable(channelConfig.getReadTimeout()) + .map(Duration::ofMillis) + .orElse(null); + if (duration != null) { // Channel config has higher priority + return duration; + } + + // less than 3.4.0, there is no org.springframework.boot.http.client.ClientHttpRequestFactorySettings + if (isSpringBootVersionLessThan340()) { + return null; + } + + // Spring Boot 3.4.0+ + var settings = beanFactory + .getBeanProvider(org.springframework.boot.http.client.ClientHttpRequestFactorySettings.class) + .getIfUnique(org.springframework.boot.http.client.ClientHttpRequestFactorySettings::defaults); + return settings.readTimeout(); + } + private RestClient buildRestClient(HttpExchangeProperties.Channel channelConfig) { // Do not use RestClient.Builder bean here, because we can't know requestFactory is configured by user or not RestClient.Builder builder = RestClient.builder(); - beanFactory - .getBeanProvider(RestClientBuilderConfigurer.class) - .ifUnique(configurer -> configurer.configure(builder)); + + configureRestClientBuilder(builder, channelConfig); + if (StringUtils.hasText(channelConfig.getBaseUrl())) { builder.baseUrl(getRealBaseUrl(channelConfig)); } @@ -322,12 +350,14 @@ private RestClient buildRestClient(HttpExchangeProperties.Channel channelConfig) header.getKey(), header.getValues().toArray(String[]::new))); } - ClientHttpRequestFactory requestFactory = - unwrapRequestFactoryIfNecessary(getFieldValue(builder, "requestFactory")); - if (requestFactory == null) { - builder.requestFactory(getRequestFactory(channelConfig)); - } else { - setTimeoutByConfig(requestFactory, channelConfig); + if (isSpringBootVersionLessThan340()) { + ClientHttpRequestFactory requestFactory = + unwrapRequestFactoryIfNecessary(getFieldValue(builder, "requestFactory")); + if (requestFactory == null) { + builder.requestFactory(getRequestFactory(channelConfig)); + } else { + setTimeoutByConfig(requestFactory, channelConfig); + } } if (isLoadBalancerEnabled(channelConfig)) { @@ -357,6 +387,61 @@ private RestClient buildRestClient(HttpExchangeProperties.Channel channelConfig) return builder.build(); } + private void configureRestClientBuilder(RestClient.Builder builder, HttpExchangeProperties.Channel channelConfig) { + var configurer = beanFactory + .getBeanProvider(RestClientBuilderConfigurer.class) + .getIfUnique(RestClientBuilderConfigurer::new); + + // requestFactorySettings have been available since Spring Boot 3.4.0 + var f = ReflectionUtils.findField(RestClientBuilderConfigurer.class, "requestFactorySettings"); + if (f != null) { + var copied = ConfigurerCopier.copyRestClientBuilderConfigurer(configurer); + ConfigurerCopier.setRestClientBuilderConfigurerProperty( + copied, "requestFactorySettings", getClientHttpRequestFactorySettings(channelConfig)); + + configurer = copied; + } + + configurer.configure(builder); + } + + private RestTemplateBuilder configureRestTemplateBuilder( + RestTemplateBuilder builder, HttpExchangeProperties.Channel channelConfig) { + RestTemplateBuilderConfigurer configurer = beanFactory + .getBeanProvider(RestTemplateBuilderConfigurer.class) + .getIfUnique(RestTemplateBuilderConfigurer::new); + + // requestFactorySettings have been available since Spring Boot 3.4.0 + var f = ReflectionUtils.findField(RestTemplateBuilderConfigurer.class, "requestFactorySettings"); + if (f != null) { + var copied = ConfigurerCopier.copyRestTemplateBuilderConfigurer(configurer); + ConfigurerCopier.setRestTemplateBuilderConfigurerProperty( + copied, "requestFactorySettings", getClientHttpRequestFactorySettings(channelConfig)); + + configurer = copied; + } + + return configurer.configure(builder); + } + + private org.springframework.boot.http.client.ClientHttpRequestFactorySettings getClientHttpRequestFactorySettings( + HttpExchangeProperties.Channel channelConfig) { + var settings = beanFactory + .getBeanProvider(org.springframework.boot.http.client.ClientHttpRequestFactorySettings.class) + .getIfUnique(org.springframework.boot.http.client.ClientHttpRequestFactorySettings::defaults); + if (channelConfig.getConnectTimeout() != null) { + settings = settings.withConnectTimeout(Duration.ofMillis(channelConfig.getConnectTimeout())); + } + if (channelConfig.getReadTimeout() != null) { + settings = settings.withReadTimeout(Duration.ofMillis(channelConfig.getReadTimeout())); + } + return settings; + } + + private static boolean isSpringBootVersionLessThan340() { + return SpringBootVersion.getVersion().compareTo("3.4.0") < 0; + } + private ClientHttpRequestFactory getRequestFactory(HttpExchangeProperties.Channel channelConfig) { ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings( Optional.ofNullable(channelConfig.getConnectTimeout()) diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeProperties.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeProperties.java index 75ee91e4..b2036b40 100644 --- a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeProperties.java +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeProperties.java @@ -12,11 +12,15 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.http.client.HttpClientProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.service.annotation.HttpExchange; /** * Http Clients Configuration Properties. @@ -89,25 +93,38 @@ public class HttpExchangeProperties implements InitializingBean { */ private ClientType clientType; /** - * whether to process {@link org.springframework.web.bind.annotation.RequestMapping} based annotation, + * whether to process {@link RequestMapping} based annotation, * default {@code false}. * - *

Recommending to use {@link org.springframework.web.service.annotation.HttpExchange} instead of {@link org.springframework.web.bind.annotation.RequestMapping}. + *

Recommending to use {@link HttpExchange} instead of {@link RequestMapping}. * * @since 3.2.0 + * @deprecated From Spring Boot 3.4.0, prefer to use {@link HttpExchange} instead of {@link RequestMapping}, + * this configuration will be removed in the 3.5.0. */ + @Deprecated(since = "3.4.0", forRemoval = true) private boolean requestMappingSupportEnabled = false; /** * Connect timeout duration, specified in milliseconds. * + * @see HttpClientProperties#connectTimeout * @since 3.2.0 + * @deprecated From Spring Boot 3.4.0, prefer {@code spring.http.client.connect-timeout}, + * this configuration will override {@code spring.http.client.connect-timeout}, + * this configuration will be removed in the 3.5.0. */ + @Deprecated(since = "3.4.0", forRemoval = true) private Integer connectTimeout; /** * Read timeout duration, specified in milliseconds. * + * @see HttpClientProperties#readTimeout * @since 3.2.0 + * @deprecated From Spring Boot 3.4.0, prefer {@code spring.http.client.read-timeout}, + * this configuration will override {@code spring.http.client.read-timeout}, + * this configuration will be removed in the 3.5.0. */ + @Deprecated(since = "3.4.0", forRemoval = true) private Integer readTimeout; /** * Whether to check unused configuration, default {@code true}. @@ -136,6 +153,22 @@ public class HttpExchangeProperties implements InitializingBean { */ private boolean httpClientReuseEnabled = true; + @DeprecatedConfigurationProperty( + since = "3.4.0", + reason = "Use 'spring.http.client.connect-timeout' instead", + replacement = "spring.http.client.connect-timeout") + public Integer getConnectTimeout() { + return connectTimeout; + } + + @DeprecatedConfigurationProperty( + since = "3.4.0", + reason = "Use 'spring.http.client.read-timeout' instead", + replacement = "spring.http.client.read-timeout") + public Integer getReadTimeout() { + return readTimeout; + } + @Data @NoArgsConstructor @AllArgsConstructor diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeRuntimeHintsRegistrar.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeRuntimeHintsRegistrar.java index d7676332..2fe123c5 100644 --- a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeRuntimeHintsRegistrar.java +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/HttpExchangeRuntimeHintsRegistrar.java @@ -11,11 +11,15 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequestFactory; @@ -39,6 +43,13 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) reflection.registerType(HttpServiceProxyFactory.Builder.class, DECLARED_FIELDS); registerForClientHttpRequestFactories(reflection); + + // See ConfigurerCopier + reflection.registerType(RestClientBuilderConfigurer.class, MemberCategory.values()); + reflection.registerType(RestTemplateBuilderConfigurer.class, MemberCategory.values()); + + // I don't know this is necessary; maybe Spring uses it for reflection? + reflection.registerType(ClientHttpRequestFactorySettings.class, MemberCategory.values()); } private void registerForClientHttpRequestFactories(ReflectionHints reflection) { diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/RequestConfigurator.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/RequestConfigurator.java index 0b3c6ede..02c40a6d 100644 --- a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/RequestConfigurator.java +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/RequestConfigurator.java @@ -33,8 +33,10 @@ * @see RequestConfiguratorBeanPostProcessor * @see Requester * @since 3.2.1 + * @deprecated from 3.4.0, too hacky, this will be removed in 3.5.0. */ @SuppressWarnings("unchecked") +@Deprecated(since = "3.4.0", forRemoval = true) public interface RequestConfigurator> { /** diff --git a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/Requester.java b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/Requester.java index df69ddf1..a0faf8a5 100644 --- a/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/Requester.java +++ b/httpexchange-spring-boot-autoconfigure/src/main/java/io/github/danielliu1123/httpexchange/Requester.java @@ -20,7 +20,10 @@ * @author Freeman * @see RequestConfigurator * @since 3.2.1 + * @deprecated from 3.4.0, Spring does not provide a way to set read timeout for a single request, this implementation is too hacky. + * This will be removed in 3.5.0. */ +@Deprecated(since = "3.4.0", forRemoval = true) public final class Requester { private Requester() {} diff --git a/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/ConfigurerCopierTest.java b/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/ConfigurerCopierTest.java new file mode 100644 index 00000000..4d10491c --- /dev/null +++ b/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/ConfigurerCopierTest.java @@ -0,0 +1,114 @@ +package io.github.danielliu1123.httpexchange; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * {@link ConfigurerCopier} + */ +class ConfigurerCopierTest { + + @Test + void makeSureConfigurerPropertiesAreNotChanged() { + + assertThat(RestClientBuilderConfigurer.class) + .hasOnlyDeclaredFields("requestFactoryBuilder", "requestFactorySettings", "customizers"); + + assertThat(RestTemplateBuilderConfigurer.class) + .hasOnlyDeclaredFields( + "requestFactoryBuilder", + "requestFactorySettings", + "httpMessageConverters", + "restTemplateCustomizers", + "restTemplateRequestCustomizers"); + } + + /** + * {@link ConfigurerCopier#copyRestClientBuilderConfigurer(RestClientBuilderConfigurer)} + */ + @Test + void testCopyRestClientBuilderConfigurer() { + + var configurer = new RestClientBuilderConfigurer(); + + ReflectionTestUtils.setField(configurer, "requestFactoryBuilder", ClientHttpRequestFactoryBuilder.jdk()); + ReflectionTestUtils.setField(configurer, "requestFactorySettings", ClientHttpRequestFactorySettings.defaults()); + ReflectionTestUtils.setField(configurer, "customizers", List.of()); + + var result = ConfigurerCopier.copyRestClientBuilderConfigurer(configurer); + + assertThat(result).isNotSameAs(configurer); + assertThat(ReflectionTestUtils.getField(result, "requestFactoryBuilder")) + .isSameAs(ReflectionTestUtils.getField(configurer, "requestFactoryBuilder")); + assertThat(ReflectionTestUtils.getField(result, "requestFactorySettings")) + .isSameAs(ReflectionTestUtils.getField(configurer, "requestFactorySettings")); + assertThat(ReflectionTestUtils.getField(result, "customizers")) + .isSameAs(ReflectionTestUtils.getField(configurer, "customizers")); + } + + /** + * {@link ConfigurerCopier#copyRestTemplateBuilderConfigurer(RestTemplateBuilderConfigurer)} + */ + @Test + void testCopyRestTemplateBuilderConfigurer() { + + var configurer = new RestTemplateBuilderConfigurer(); + + ReflectionTestUtils.setField(configurer, "requestFactoryBuilder", ClientHttpRequestFactoryBuilder.jdk()); + ReflectionTestUtils.setField(configurer, "requestFactorySettings", ClientHttpRequestFactorySettings.defaults()); + ReflectionTestUtils.setField(configurer, "httpMessageConverters", null); + ReflectionTestUtils.setField(configurer, "restTemplateCustomizers", List.of()); + ReflectionTestUtils.setField(configurer, "restTemplateRequestCustomizers", List.of()); + + var result = ConfigurerCopier.copyRestTemplateBuilderConfigurer(configurer); + + assertThat(result).isNotSameAs(configurer); + assertThat(ReflectionTestUtils.getField(result, "requestFactoryBuilder")) + .isSameAs(ReflectionTestUtils.getField(configurer, "requestFactoryBuilder")); + assertThat(ReflectionTestUtils.getField(result, "requestFactorySettings")) + .isSameAs(ReflectionTestUtils.getField(configurer, "requestFactorySettings")); + assertThat(ReflectionTestUtils.getField(result, "httpMessageConverters")) + .isSameAs(ReflectionTestUtils.getField(configurer, "httpMessageConverters")); + assertThat(ReflectionTestUtils.getField(result, "restTemplateCustomizers")) + .isSameAs(ReflectionTestUtils.getField(configurer, "restTemplateCustomizers")); + assertThat(ReflectionTestUtils.getField(result, "restTemplateRequestCustomizers")) + .isSameAs(ReflectionTestUtils.getField(configurer, "restTemplateRequestCustomizers")); + } + + /** + * {@link ConfigurerCopier#setRestClientBuilderConfigurerProperty(RestClientBuilderConfigurer, String, Object)} + */ + @Test + void testSetRestClientBuilderConfigurerProperty() { + var configurer = new RestClientBuilderConfigurer(); + + var fb = ClientHttpRequestFactoryBuilder.jdk(); + + ConfigurerCopier.setRestClientBuilderConfigurerProperty(configurer, "requestFactoryBuilder", fb); + + assertThat(ReflectionTestUtils.getField(configurer, "requestFactoryBuilder")) + .isSameAs(fb); + } + + /** + * {@link ConfigurerCopier#setRestTemplateBuilderConfigurerProperty(RestTemplateBuilderConfigurer, String, Object)} + */ + @Test + void testSetRestTemplateBuilderConfigurerProperty() { + var configurer = new RestTemplateBuilderConfigurer(); + + var fb = ClientHttpRequestFactoryBuilder.jdk(); + + ConfigurerCopier.setRestTemplateBuilderConfigurerProperty(configurer, "requestFactoryBuilder", fb); + + assertThat(ReflectionTestUtils.getField(configurer, "requestFactoryBuilder")) + .isSameAs(fb); + } +} diff --git a/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/TimeoutTests.java b/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/TimeoutTests.java index 15a9ffed..38c7737f 100644 --- a/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/TimeoutTests.java +++ b/httpexchange-spring-boot-autoconfigure/src/test/java/io/github/danielliu1123/httpexchange/TimeoutTests.java @@ -5,12 +5,14 @@ import io.github.danielliu1123.PortGetter; import io.github.danielliu1123.httpexchange.shaded.requestfactory.EnhancedJdkClientHttpRequestFactory; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeoutException; import lombok.SneakyThrows; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.http.client.HttpClientProperties; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,7 +32,7 @@ void testDefaultTimeout_whenExceed(String clientType) { int port = PortGetter.availablePort(); try (var ctx = new SpringApplicationBuilder(TimeoutConfig.class) .properties("server.port=" + port) - .properties(HttpExchangeProperties.PREFIX + ".read-timeout=100") + .properties("spring.http.client.read-timeout=100ms") .properties(HttpExchangeProperties.PREFIX + ".client-type=" + clientType) .properties(HttpExchangeProperties.PREFIX + ".base-url=localhost:" + port) .run()) { @@ -48,7 +50,7 @@ void testDefaultTimeout_whenExceedUsingWebClient_thenTimeoutException(String cli int port = PortGetter.availablePort(); var ctx = new SpringApplicationBuilder(TimeoutConfig.class) .properties("server.port=" + port) - .properties(HttpExchangeProperties.PREFIX + ".read-timeout=100") + .properties("spring.http.client.read-timeout=100ms") .properties(HttpExchangeProperties.PREFIX + ".client-type=" + clientType) .properties(HttpExchangeProperties.PREFIX + ".base-url=localhost:" + port) .run(); @@ -60,12 +62,12 @@ void testDefaultTimeout_whenExceedUsingWebClient_thenTimeoutException(String cli } @ParameterizedTest - @ValueSource(strings = {"REST_CLIENT", "REST_TEMPLATE", "WEB_CLIENT"}) + @ValueSource(strings = {"REST_CLIENT", "REST_TEMPLATE"}) void testDefaultTimeout_whenNotExceed(String clientType) { int port = PortGetter.availablePort(); var ctx = new SpringApplicationBuilder(TimeoutConfig.class) .properties("server.port=" + port) - .properties(HttpExchangeProperties.PREFIX + ".read-timeout=100") + .properties("spring.http.client.read-timeout=100ms") .properties(HttpExchangeProperties.PREFIX + ".client-type=" + clientType) .properties(HttpExchangeProperties.PREFIX + ".base-url=localhost:" + port) .run(); @@ -77,12 +79,12 @@ void testDefaultTimeout_whenNotExceed(String clientType) { } @ParameterizedTest - @ValueSource(strings = {"REST_CLIENT", "REST_TEMPLATE", "WEB_CLIENT"}) + @ValueSource(strings = {"REST_CLIENT", "REST_TEMPLATE"}) void testTimeout_whenNotExceed(String clientType) { int port = PortGetter.availablePort(); var ctx = new SpringApplicationBuilder(TimeoutConfig.class) .properties("server.port=" + port) - .properties(HttpExchangeProperties.PREFIX + ".read-timeout=100") + .properties("spring.http.client.read-timeout=100ms") .properties(HttpExchangeProperties.PREFIX + ".client-type=" + clientType) .properties(HttpExchangeProperties.PREFIX + ".channels[0].base-url=http://localhost:" + port) .properties(HttpExchangeProperties.PREFIX + ".channels[0].clients[0]=DelayApi") @@ -101,7 +103,7 @@ void testTimeoutForSingleRequest_whenUsingBlockingClient_thenWorksFine(String cl int port = PortGetter.availablePort(); var ctx = new SpringApplicationBuilder(TimeoutConfig.class) .properties("server.port=" + port) - .properties(HttpExchangeProperties.PREFIX + ".read-timeout=100") + .properties("spring.http.client.read-timeout=100ms") .properties(HttpExchangeProperties.PREFIX + ".client-type=" + clientType) .properties(HttpExchangeProperties.PREFIX + ".base-url=localhost:" + port) .run(); @@ -146,19 +148,25 @@ interface DelayApi extends RequestConfigurator { static class TimeoutConfig implements DelayApi { @Bean - HttpClientCustomizer.RestClientCustomizer restClientCustomizer() { + HttpClientCustomizer.RestClientCustomizer restClientCustomizer(HttpClientProperties httpClientProperties) { return (client, channel) -> { EnhancedJdkClientHttpRequestFactory requestFactory = new EnhancedJdkClientHttpRequestFactory(); - Optional.ofNullable(channel.getReadTimeout()).ifPresent(requestFactory::setReadTimeout); + var readTimeout = Optional.ofNullable(channel.getReadTimeout()) + .map(Duration::ofMillis) + .orElse(httpClientProperties.getReadTimeout()); + Optional.ofNullable(readTimeout).ifPresent(requestFactory::setReadTimeout); client.requestFactory(requestFactory); }; } @Bean - HttpClientCustomizer.RestTemplateCustomizer restTemplateCustomizer() { + HttpClientCustomizer.RestTemplateCustomizer restTemplateCustomizer(HttpClientProperties httpClientProperties) { return (client, channel) -> { EnhancedJdkClientHttpRequestFactory requestFactory = new EnhancedJdkClientHttpRequestFactory(); - Optional.ofNullable(channel.getReadTimeout()).ifPresent(requestFactory::setReadTimeout); + var readTimeout = Optional.ofNullable(channel.getReadTimeout()) + .map(Duration::ofMillis) + .orElse(httpClientProperties.getReadTimeout()); + Optional.ofNullable(readTimeout).ifPresent(requestFactory::setReadTimeout); client.setRequestFactory(requestFactory); }; } diff --git a/website/docs/40-configuration-properties.md b/website/docs/40-configuration-properties.md index 6ddaa8e2..e4bfebd1 100644 --- a/website/docs/40-configuration-properties.md +++ b/website/docs/40-configuration-properties.md @@ -20,24 +20,24 @@ This page was generated by [spring-configuration-property-documenter](https://gi |Key|Type|Description|Default value|Deprecation| |---|----|-----------|-------------|-----------| -| base-packages| java.util.Set<java.lang.String>| Base packages to scan, use \{@link EnableExchangeClients#basePackages} first if configured.| | | -| base-url| java.lang.String| Default base url, 'http' scheme can be omitted. <p> If loadbalancer is enabled, this value means the service id. <ul> <li> localhost:8080 </li> <li> http://localhost:8080 </li> <li> https://localhost:8080 </li> <li> localhost:8080/api </li> <li> user(service id) </li> </ul>| | | -| bean-to-query-enabled| java.lang.Boolean| Whether to convert Java bean to query parameters, default value is \{@code false}.| false| | -| channels| java.util.List<io.github.danielliu1123.httpexchange.HttpExchangeProperties$Channel>| Channels configuration.| | | -| client-type| io.github.danielliu1123.httpexchange.HttpExchangeProperties$ClientType| Client Type, if not specified, an appropriate client type will be set. <ul> <li> Use \{@link ClientType#REST_CLIENT} if none of the methods in the client return Reactive type. <li> Use \{@link ClientType#WEB_CLIENT} if any method in the client returns Reactive type. </ul> <p> In most cases, there's no need to explicitly specify the client type. <p color="orange"> NOTE: the \{@link #connectTimeout} and \{@link #readTimeout} settings are not supported by \{@link ClientType#WEB_CLIENT}. @see ClientType @since 3.2.0| | | -| clients| java.util.Set<java.lang.Class<?>>| Exchange client interfaces to register as beans, use \{@link EnableExchangeClients#clients} first if configured. @since 3.2.0| | | -| connect-timeout| java.lang.Integer| Connect timeout duration, specified in milliseconds. @since 3.2.0| | | -| enabled| java.lang.Boolean| Whether to enable http exchange autoconfiguration, default \{@code true}.| true| | -| headers| java.util.List<io.github.danielliu1123.httpexchange.HttpExchangeProperties$Header>| Default headers will be added to all the requests.| | | -| http-client-reuse-enabled| java.lang.Boolean| Whether to enable http client reuse, default \{@code true}. <p> Same \{@link Channel} configuration will share the same http client if enabled. @since 3.2.2| true| | -| loadbalancer-enabled| java.lang.Boolean| Whether to enable loadbalancer, default \{@code true}. <p> Prerequisites: <ul> <li> \{@code spring-cloud-starter-loadbalancer} dependency in the classpath.</li> <li> \{@code spring.cloud.loadbalancer.enabled=true}</li> </ul> @since 3.2.0| true| | -| read-timeout| java.lang.Integer| Read timeout duration, specified in milliseconds. @since 3.2.0| | | -| request-mapping-support-enabled| java.lang.Boolean| whether to process \{@link org.springframework.web.bind.annotation.RequestMapping} based annotation, default \{@code false}. <p color="red"> Recommending to use \{@link org.springframework.web.service.annotation.HttpExchange} instead of \{@link org.springframework.web.bind.annotation.RequestMapping}. @since 3.2.0| false| | -| warn-unused-config-enabled| java.lang.Boolean| Whether to check unused configuration, default \{@code true}. @since 3.2.0| true| | +| base-packages| java.util.Set<java.lang.String>| | | | +| base-url| java.lang.String| | | | +| bean-to-query-enabled| java.lang.Boolean| | | | +| channels| java.util.List<io.github.danielliu1123.httpexchange.HttpExchangeProperties$Channel>| | | | +| client-type| io.github.danielliu1123.httpexchange.HttpExchangeProperties$ClientType| | | | +| clients| java.util.Set<java.lang.Class<?>>| | | | +| connect-timeout| java.lang.Integer| | | Reason: Use 'spring.http.client.connect-timeout' instead, use for replacement: spring.http.client.connect-timeout| +| enabled| java.lang.Boolean| | | | +| headers| java.util.List<io.github.danielliu1123.httpexchange.HttpExchangeProperties$Header>| | | | +| http-client-reuse-enabled| java.lang.Boolean| | | | +| loadbalancer-enabled| java.lang.Boolean| | | | +| read-timeout| java.lang.Integer| | | Reason: Use 'spring.http.client.read-timeout' instead, use for replacement: spring.http.client.read-timeout| +| request-mapping-support-enabled| java.lang.Boolean| | | Reason: null, use for replacement: null| +| warn-unused-config-enabled| java.lang.Boolean| | | | ### http-exchange.refresh **Class:** `io.github.danielliu1123.httpexchange.HttpExchangeProperties$Refresh` |Key|Type|Description|Default value|Deprecation| |---|----|-----------|-------------|-----------| -| enabled| java.lang.Boolean| Whether to enable refresh exchange clients, default \{@code false}. <p> This feature needs \{@code spring-cloud-context} dependency in the classpath. <p color="orange"> NOTE: This feature is not supported by native image. @see <a href="/~https://github.com/spring-cloud/spring-cloud-release/wiki/AOT-transformations-and-native-image-support#refresh-scope">Refresh Scope</a>| false| | +| enabled| java.lang.Boolean| | | |